Любая сессия в HTTP держится на печенье. Сервер отправляет заголовок Set-Cookie, клиент обязан сохранить его и вернуть в следующем запросе, иначе бэкенд при каждом обращении видит перед собой незнакомца. Браузер делает всю эту работу молча, а curl без соответствующей просьбы выбрасывает cookies сразу после завершения команды. Отсюда классическая ловушка при проверке API: вход проходит успешно, а следующий запрос приносит 401, тестировщик часами читает серверную логику, хотя проблема сидит во флагах клиента.
Cookie-jar решает её одним движением. Флаг -c складывает всё полученное в файл, флаг -b читает файл обратно, и набор разрозненных команд превращается в связный сценарий со сквозной сессией. Пара -b и -c читается как помнить и записывать, и запоминание по этой паре работает лучше, чем по длинным именам --cookie и --cookie-jar. Дальше разберём формат jar, живые сценарии входа, работу с несколькими доменами и типовые промахи, из-за которых тестовые сессии рассыпаются.
Формат файла jar и поля в каждой строке
Флаг -c, он же --cookie-jar, записывает cookies в файл, а флаг -b, он же --cookie, читает их. Формат исторически называют Netscape: строка заголовка, затем по строке на каждый cookie, поля разделены табуляцией. curl записывает в начало файла две строки комментариев, включая предупреждение "Edit at your own risk", и дальше обслуживает файл сам.
# Netscape HTTP Cookie File
# This file was generated by libcurl! Edit at your own risk.
.example.com TRUE / FALSE 1747814400 sid 8f3e2c1a
.example.com TRUE /api FALSE 0 csrf 77aa91
Первое поле задаёт домен, точка впереди означает распространение на поддомены. Дальше идут признак доменного cookie, путь, флаг secure, срок жизни в Unix-секундах, имя и значение. Нулевой срок означает сессионный cookie: он живёт до завершения процесса curl и сохраняется в файл разве что для порядка. Строки с префиксом #HttpOnly_ кодируют одноимённый флаг, и curl обрабатывает их наравне с остальными, а рукописные парсеры их регулярно теряют. Чужой домен в jar не мешает: curl просто не отправит его cookie, поэтому один файл спокойно хранит сразу несколько сервисов.
Файл jar можно править и руками, но осторожно: разделитель полей строго табуляция, и после правки в редакторе с пробелами curl молча пропустит испорченные строки. Для разовых проверок jar не обязателен: -b "sid=abc123; theme=dark" отправляет cookies строкой прямо из командной строки, а комбинация -b jar.txt -b "debug=1" добавляет отладочный cookie поверх содержимого файла. Такой приём выручает, когда нужно проверить ветку поведения, зависящую от одного значения, не трогая остальную сессию.
Вход в приложение и сохранение сессии
Живой сценарий начинается с авторизации. Пусть API принимает POST на /auth/login и отвечает парой cookie: идентификатор сессии и свежий CSRF-токен. Команда выглядит так.
curl -sS -o /dev/null -w "код %{http_code}
" -c jar.txt -d "username=qa-robot&password=strong-pass-9" https://api.example.com/auth/login
Флаг -d собрал форму, -c записал jar, а строка формата -w показала код ответа, чтобы скрипт сразу понял, принят ли вход. Пара -sS глушит индикатор прогресса, но оставляет ошибки соединения на экране, это лучший режим для автоматизации. Дальше любая команда с -b jar.txt приносит серверу session id, и бэкенд наконец узнаёт клиента. Для уверенности полезно заглянуть в сам файл: grep по имени cookie подтверждает, что Set-Cookie дошёл до клиента, а не был срезан промежуточным сервером по дороге. Отдельная деталь, о которой часто забывают: панели защиты сверяют заголовок User-Agent, поэтому в тестах его стоит фиксировать через -A тем же значением, что и в браузерной сессии.
Схемы с CSRF-токеном требуют двух шагов: сначала GET страницы входа с -c, затем извлечение токена и POST с ним. Токен вытаскивают связкой grep и sed прямо в переменную и подставляют в -d. Если API ожидает JSON, форму заменяет -d @login.json вместе с заголовком -H "Content-Type: application/json", а сам файл удобно держать в репозитории рядом со сценарием. Оба варианта работают с jar одинаково, разница только в теле запроса. Отдельно проверяют срок жизни: значение в ответе сравнивают с ожидаемым, и сессия, которая живёт минуту вместо недели, меняет весь сценарий тестов.
Методы расширяют сценарий: HEAD через -I подтверждает наличие файла без тела, OPTIONS показывает разрешённые методы в заголовке Allow, а DELETE проверяет удаление и последующий 404. Серия запросов по списку идентификаторов собирает статистику: счётчики кодов в одной строке говорят о здоровье API больше, чем отдельный запрос.
for id in $(seq 101 110); do
curl -s -o /dev/null -w "%{http_code}
" -b jar.txt \
"https://api.example.com/v1/orders/$id"
done | sort | uniq -c
Строка вида 10 200 означает десять успешных ответов, и появление 500 в списке сразу видно на фоне единообразного вывода. В конвейере тестов такой мини-отчёт прикладывают к шагу проверки, и регрессия по конкретному коду читается без разбора логов.
Чтение и запись jar в одной команде
Сессии не статичны. Ротация идентификатора после смены привилегий, обновление CSRF-токена, продление срока жизни, всё это приходит в ответах свежими Set-Cookie. Если читать jar без записи, обновления теряются, и через несколько шагов сценарий рассыпается с загадочными 403. Практика проста: указывать один и тот же файл и в -b, и в -c.
curl -sS -b jar.txt -c jar.txt https://api.example.com/v1/orders
curl сначала загружает cookies из файла в память, затем выполняет запрос и перезаписывает jar актуальным состоянием. Порядок безопасен, запись происходит после обработки ответа. Исключение, о котором стоит помнить, это параллельный запуск десятков curl в один jar: процессы затирают состояние друг друга, и гонку лучше разводить по отдельным файлам с именами вида jar-$pid.txt, сливая их уже в конце прогона. Для отладки есть трюк: --cookie-jar со знаком минус вместо имени файла печатает jar прямо в stdout, и содержимое видно в живом виде. Когда параллельных воркеров много, каждому выдают собственный jar с номером в имени, а итоговое состояние собирают после прогона. curl не умеет сливать два jar сам, поэтому строки просто склеивают и убирают повторяющиеся пары имя-значение по домену и пути. Права на файл заслуживают внимания: jar содержит рабочую сессию, эквивалентную паролю до конца её жизни, поэтому chmod 600 и приватный каталог для артефактов входят в гигиену наравне с ротацией. Опция --create-dirs спасает, когда jar кладут в свежесозданный каталог артефактов: недостающие папки создаются без отдельного mkdir.
Проверка защищённых эндпоинтов и истёкших сессий
Тест авторизации считается полным, когда проверены обе стороны: допуск со свежей сессией и отказ с испорченной. Код ответа удобно снимать строкой формата, не тащя тело в консоль.
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 -b jar.txt https://api.example.com/v1/profile)
echo "$code"
Ожидание простое: 200 со свежим jar и 401 после подмены значения session id. Тело ответа сверяют точечно: jq по ключу достовернее grep-а по подстроке, потому что слово может встретиться в неожиданном месте, а структура JSON иногда меняется между версиями API. Подмену делает одна команда sed, меняющая символ в последнем поле нужной строки, и сценарий покрывает ветку отказа, не дожидаясь настоящего таймаута сессии. Флаг -L включает прохождение редиректов, а -j, он же --junk-session-cookies, отбрасывает сессионные cookie при переходе между хостами. Задержки измеряет %{time_total}: значение выше согласованного лимита роняет проверку вместе с кодом ответа. Отдельного внимания заслуживают коды выхода самого curl: 7 означает отказ в соединении, 28 превышение --max-time, и скрипт с set -e остановится на первом же сетевом сбое, что в CI как раз и нужно.
Просроченные cookie curl убирает сам при загрузке jar: строки с истёкшим сроком в запросы не попадают. Отсюда удобный приём для отрицательных проверок: в поле срока жизни вписывают вчерашнюю дату в Unix-секундах, посчитанную командой date -d yesterday +%s, и сервер отвечает 401 без правки значения. Ручной чистки jar это не отменяет: файл, который растёт месяцами, тормозит разве что чтение, зато путаницы в чужих доменах добавляет.
Cookies нескольких доменов в одном jar
Микросервисные схемы любят разносить вход и содержимое по разным хостам: авторизация на accounts.example.com, товары на api.example.com, личный кабинет на app.example.com. Один jar хранит их всех и раздаёт адресно, по домену и пути.
curl -sS -c jar.txt -d "user=qa&pass=qa" https://accounts.example.com/login
curl -sS -b jar.txt -c jar.txt https://app.example.com/dashboard
Полезно помнить, что cookie не эквивалентны токенам в заголовках. Если API ожидает Authorization, его добавляют флагом -H рядом с cookies, и они не мешают друг другу. Путевые ограничения работают и в обратную сторону: cookie для /admin не уедет на /public, даже когда домен совпадает. С редиректами между хостами связана ловушка: обычный -L при переходе на чужой домен cookies не передаёт, а --location-trusted передаёт любые учётные сведения всем адресам цепочки, поэтому этот флаг уместен только внутри своего стенда. Заглянуть в jar перед сомнительным шагом проще, чем расшифровывать 403 из логов. Флаги безопасности браузеров curl трактует по-своему. Secure означает только отправку по HTTPS, HttpOnly в терминале лишён смысла, а атрибут SameSite curl игнорирует целиком: это чисто браузерный механизм защиты от межсайтовых запросов. Для тестов это удобно, но помнить о разнице полезно: поведение curl с cookie никогда не совпадёт с браузером в точку, если приложение полагается на SameSite.
Скрипт для CI со сквозным сценарием
В конвейере тестов цепочка curl склеивается в маленький скрипт, который падает на первом отклонении от ожиданий. Порядок шагов выглядит так:
- стереть jar перед прогоном, чтобы старые сессии не подмешивались;
- авторизоваться с -c и убедиться по коду ответа, что вход принят;
- запросить защищённый эндпоинт с -b и -c и дождаться 200;
- испортить значение сессионного cookie в jar и получить 401;
- пройти вход заново и подтвердить, что новая сессия открывает те же эндпоинты.
#!/usr/bin/env bash
set -euo pipefail
base=https://api.example.com
rm -f jar.txt
curl -sS -c jar.txt -d "user=ci&pass=ci" "$base/login"
curl -sS -b jar.txt -c jar.txt "$base/v1/profile" | grep -q '"login":"ci"'
echo "сессия прошла"
Строгий режим set -euo pipefail останавливает скрипт на первой же ошибке сети или ненулевом коде. Для сложных цепочек каркас расширяется проверками времени ответа, сравнением JSON по ключам и фиксацией трассировки: --trace-ascii trace.txt складывает заголовки в файл, который удобно прикладывать к отчёту о падении.
Типовые ошибки и быстрые проверки
Часть промахов повторяется настолько регулярно, что их полезно знать в лицо. Забытый -b оставляет запрос анонимным, и сервер честно отвечает 401. Чтение без записи пропускает ротацию токенов. Заголовок Cookie, собранный руками через -H "Cookie: sid=x", живёт до первого HttpOnly и до первого пробела в значении, тогда как jar переносит всё. Общая папка tmp в контейнере стирает jar между шагами, и сценарий входит заново, сами того не замечая. Время на сервере, ушедшее вперёд, обнуляет свежие cookies молча: клиент отправляет исправный cookie, а сервер считает его просроченным.
Диагностика занимает секунды. Флаг -v показывает отправленные и полученные заголовки целиком, --trace-ascii пишет их в файл для сравнения двух прогонов, разовый --cookie "sid=broken" подменяет конкретное значение без правки jar. К миганию сети в прогонах готовит связка --retry 3 --retry-all-errors: curl повторит неудачу трижды, прежде чем уронить шаг. Для медленных соседей по конвейеру стоят --connect-timeout 5 и общий --max-time 30, и сценарий перестаёт зависать на полуоткрытых соединениях. Cookie-jar не отменяет понимания того, как живёт сессия, но снимает всю механическую часть. Два флага превращают разрозненные вызовы в настоящий сценарий, и поиск ошибок авторизации сдвигается с клиента туда, где ему и место, в серверную логику.