Заголовок
Проверка
Куда браузер её отправит
Что это такое
Что происходит. Сервер отвечает заголовком Set-Cookie — это просьба к браузеру запомнить значение и присылать его обратно. Браузер может просьбу и отклонить, причём молча: ни ошибки, ни строчки в консоли. Отсюда и берётся «сервер её ставит, а её нет».
Где взять строку. Откройте DevTools → Network и включите Preserve log — без него запрос, который ставит cookie, исчезнет при переходе на следующую страницу. Повторите действие (вход, обновление токена). Выберите запрос → вкладка Headers → раздел Response Headers → строка set-cookie.
Быстрее: правый клик по запросу → Copy → Copy response headers и вставить сюда весь блок целиком. Чужие заголовки я пропущу сам и скажу, сколько строк пропустил.
Пробуете прямо на этой странице? Тогда всё верно и искать нечего: qatoolqa.ru не ставит cookie вообще — ни одной. Сервер их не отдаёт, скрипты не создают, выбор темы и размера текста лежит в localStorage. Это то самое обещание из подвала, только что проверенное вами лично. Смотреть надо на том сайте, который вы тестируете, — подойдёт любой, куда вы логинитесь.
Не нашли её в Headers на нужном сайте? Обычных причин четыре. В HTTP/2 заголовок приходит в нижнем регистре — set-cookie, и глазами по «Set-Cookie» его легко пропустить. Смотрите не на тот запрос: cookie ставит ответ на логин или редирект 302, а не загрузка страницы — включите фильтр Doc и Fetch/XHR и пройдите по ним. Ответ взят из кеша — тогда заголовков нет вовсе, перезагрузите с Ctrl+Shift+R. Либо cookie ставит не сервер, а скрипт через document.cookie — тогда заголовка нет в принципе, и искать его бесполезно.
Рядом с Headers есть вкладка Cookies — там cookie запроса и ответа уже разобраны по полям. По ней удобно убедиться, что она вообще пришла, но сырой строки оттуда не скопировать.
А вот Application → Cookies тут не поможет, и это важно: там лежит только то, что браузер принял. Отклонённой cookie там нет и не будет — именно поэтому её и не находят, когда ищут привычным способом.
Почему браузер отказывает. Причин немного, и все они в самом заголовке: SameSite=None без Secure; нарушенный префикс __Host- или __Secure-; Partitioned без Secure; больше 4096 байт на имя со значением; сломанная пара «имя=значение». Сервер об отказе не узнает — решение принимает браузер, уже получив ответ.
Что будет, если не примет. Ничего заметного, и в этом вся беда. Ответ остаётся 200, ошибки нет, в консоли пусто. Cookie просто не появляется в хранилище, и следующий запрос уходит без неё — сервер видит неавторизованного. Со стороны это разлогин, 401 или редирект на логин, то есть похоже на поломку бэкенда, хотя дело в одном атрибуте заголовка. В Chrome отклонённые заголовки видны в DevTools → Network → нужный запрос → вкладка Cookies, а причина — в панели Issues.
Может ли принять только часть. Одну cookie — нет: она сохраняется целиком или отбрасывается целиком, половин не бывает. Но в ответе обычно несколько заголовков Set-Cookie, и решение принимается по каждому отдельно — две сохранятся, третья нет. Поэтому проверять надо все, а не первую.
Ниже — что значит каждый флаг.
Secure — cookie уходит только по HTTPS. По обычному http её не увидит ни сервер, ни JavaScript.
HttpOnly — недоступна из document.cookie. Если в консоли пусто, а в запросах cookie есть — почти всегда дело в этом флаге, а не в том, что её не поставили.
SameSite — отправлять ли cookie, когда запрос идёт с чужого сайта. Strict — никогда, даже при переходе по ссылке. Lax — только при обычном переходе по ссылке, но не в фоновых запросах и не в POST. None — всегда, и это единственный вариант для встроенных виджетов и межсайтовых сценариев.
Если SameSite не указан, современные браузеры считают его Lax. Раньше умолчанием было None, и старые интеграции ломались именно на этой смене.
SameSite=None без Secure браузер отклоняет целиком — cookie не будет вообще. Это самая частая причина «сервер её ставит, а её нет».
Domain задаёт, кому cookie достанется. Указали example.com — уйдёт и на api.example.com. Не указали вовсе — останется только на том хосте, который её выдал, и на поддомены НЕ пойдёт. Это противоположно тому, чего обычно ждут.
Точка в начале (.example.com) ничего не меняет: браузеры её отбрасывают, поведение то же, что без точки.
Префиксы имени. __Secure- требует флага Secure. __Host- строже: нужен Secure, Path=/ и полное отсутствие Domain. Нарушили — браузер молча не примет cookie. Зато такую cookie не подделает соседний поддомен.
Max-Age важнее Expires. Если стоят оба, браузер смотрит на Max-Age, а Expires игнорирует.
Частые вопросы
Почему cookie не ставится?
Проверьте четыре вещи: Secure без HTTPS, SameSite=None без Secure, несовпадение Domain с текущим хостом и Path, который не покрывает страницу. Разбор покажет каждое из этих значений.
Что выбрать: Expires или Max-Age?
Max-Age задаёт срок в секундах от текущего момента и не зависит от часов клиента, Expires — абсолютную дату. Если указаны оба, современные браузеры берут Max-Age.
Как SameSite влияет на тесты?
При Lax cookie не уходит в кросс-доменных POST-запросах и в части iframe-сценариев. Многие «плавающие» баги авторизации в интеграциях — про это.