Основы веб-разработки

Куки

Кука — небольшое именованное значение, которое сервер просит браузер сохранить и присылать обратно при каждом следующем обращении к сайту. Это единственный способ, которым HTTP — протокол, ничего не помнящий между запросами, — узнаёт, что два обращения пришли от одного и того же человека.

Чтение Cookie::get()Запись Cookie::add()Описание SetCookie

Что такое кука и зачем она нужна

Проблема. HTTP не хранит состояния. Пользователь ввёл логин и пароль, сервер его узнал — а следующий запрос от того же браузера приходит как от совершенно незнакомого клиента. Всё, чем сервер располагает между двумя запросами, — это то, что он сам попросил браузер запомнить.

Решение. Сервер отправляет куку заголовком Set-Cookie, браузер её сохраняет и прикладывает заголовком Cookie ко всем последующим запросам к этому сайту. На этом устроены сессии, «запомнить меня», выбранная тема оформления и язык, корзина до регистрации, защита от подделки запросов.

Сам обмен прост. Сложность — в атрибутах, которыми кука сопровождается. Кука без HttpOnly доступна любому скрипту на странице. Кука без SameSite уезжает на чужой сайт вместе с запросом, который подделал кто-то третий. Кука без Secure идёт по открытому каналу. Ошибка в любом из них не даёт ни исключения, ни предупреждения — браузер просто ведёт себя не так, как вы ожидали, и узнаёте вы об этом позже и не оттуда.

Поэтому куки во фреймворке описываются объектом, который отказывается собираться неправильно, а не строкой заголовка, которую надо не забыть составить верно.

Быстрый старт

main/Controller/SessionController.php
use Flytachi\Winter\Kernel\Http\Cookie\Cookie;

#[PostMapping('login')]
public function login(#[RequestBody] LoginForm $form): ResponseEntity
{
  $token = $this->auth->authenticate($form->email, $form->password);

  Cookie::add(Cookie::make('sid', $token)->expiresIn(3600));

  return ResponseEntity::noContent();
}

#[GetMapping('me')]
public function me(): ResponseEntity
{
  $token = Cookie::get('sid');

  if ($token === null) {
      return ResponseEntity::unauthorized(['message' => 'Не выполнен вход']);
  }

  return ResponseEntity::ok($this->auth->userByToken($token));
}

#[PostMapping('logout')]
public function logout(): ResponseEntity
{
  Cookie::forget('sid');

  return ResponseEntity::noContent();
}

Ничего инициализировать не нужно: роутер подготавливает работу с куками в начале каждого запроса, до того как управление дойдёт до контроллера.

Один заголовок, много значений

Set-Cookie — единственный заголовок HTTP, который допустимо повторять: три куки отправляются тремя заголовками. Поэтому у кук отдельный путь в ответ, а не ->header('Set-Cookie', …): карта заголовков ключуется по имени, и вторая кука затёрла бы первую.


Справочник

Cookie — фасад для работы с куками текущего запроса. Через него читают то, что прислал браузер, и отправляют то, что должно к нему уйти.

Все методы статические, но состояние у фасада на запрос, а не на процесс: под Swoole данные живут в контексте корутины, обслуживающей запрос, и два одновременных запроса не видят кук друг друга. В режиме, где на процесс приходится один запрос, состояние хранится в статическом поле — там изоляция обеспечивается самим процессом.

Куки, которые вы отправляете, записываются в ответ сразу, ровно как заголовки, а не копятся до конца обработки. Разница проявляется на ошибке:

php
Cookie::forget('sid');   // сессия погашена — кука уже в ответе

throw new ResponseException('Сессия истекла', HttpCode::UNAUTHORIZED);

Если бы куки отправлялись в конце успешного пути, эта — самая нужная — потерялась бы именно тогда, когда запрос завершился ошибкой, и браузер остался бы с мёртвой сессией.

Cookie::get()

Возвращает значение куки, присланной браузером.

Значение приходит уже раскодированным: если при отправке оно было закодировано (а по умолчанию это так), обратное преобразование выполняется здесь.

Синтаксис

php
public static function get(string $name): ?string

Параметры

$name — имя куки. Регистр важен: именно в таком виде её прислал клиент.

Возвращает

Строку со значением, либо null, если куки с таким именем в запросе не было.

Пример

php
$theme = Cookie::get('theme') ?? 'light';

Cookie::has()

Сообщает, прислал ли браузер куку с указанным именем.

Отличается от get() !== null одним случаем, и он не надуманный: consent= — это присланная кука с пустым значением. get() вернёт пустую строку, а has() — true, и для флагов согласия или отметок «показано» разница существенная: пустая строка означает «ответил», отсутствие — «не ответил».

Синтаксис

php
public static function has(string $name): bool

Параметры

$name — имя куки.

Возвращает

true, если кука была прислана, даже с пустым значением.

Пример

php
if (!Cookie::has('cookie_consent')) {
  return ResponseView::view('banners/cookie-consent');
}

Cookie::all()

Возвращает все куки запроса.

Синтаксис

php
public static function all(): array

Возвращает

Массив «имя → значение» в том порядке, в каком куки прислал клиент. Пустой массив, если кук не было или запроса нет вовсе — например, в консольной команде.

Пример

php
foreach (Cookie::all() as $name => $value) {
  $this->logger->debug('Кука в запросе', ['name' => $name, 'length' => strlen($value)]);
}

Cookie::make()

Создаёт описание куки, готовое к отправке, с учётом текущего запроса и настроек приложения.

Отличается от SetCookie::make() двумя вещами. Во-первых, проставляет Secure, если запрос пришёл по HTTPS: объект-значение схему запроса не видит, а ошибиться здесь можно в обе стороны, и обе ошибки тихие — кука с Secure, отправленная по обычному HTTP, браузером молча выбрасывается. Во-вторых, применяет умолчания приложения, если они заданы.

Это тот вариант, который нужен прикладному коду.

Синтаксис

php
public static function make(string $name, string $value = ''): SetCookie

Параметры

$name — имя куки. Должно состоять из букв, цифр и части знаков препинания; пробел, кавычка, точка с запятой и скобки недопустимы. При нарушении будет выброшено InvalidArgumentException.

$value — значение. По умолчанию пустое. Кодируется при отправке, поэтому подставлять можно любой текст.

Возвращает

Объект SetCookie с проставленными умолчаниями. Дальше его настраивают цепочкой вызовов и отправляют через Cookie::add().

Пример

php
$cookie = Cookie::make('theme', 'dark')
  ->expiresIn(60 * 60 * 24 * 365)
  ->httpOnly(false);   // читается скриптом на странице

Cookie::add()

Отправляет куку клиенту — записывает её в ответ текущего запроса.

Запись происходит немедленно, а не в конце обработки. Поэтому кука, поставленная до выброшенного исключения, всё равно доедет до браузера.

Синтаксис

php
public static function add(SetCookie $cookie): void

Параметры

$cookie — описание куки, созданное через Cookie::make() или SetCookie::make().

Ошибки

LogicException — если вызвать вне обработки запроса, например из консольной команды. Отправлять некуда, а молча проглоченная кука выглядит для вызывающего как «браузер её проигнорировал», и отладка такого стоит несоизмеримо дороже, чем понятная ошибка.

InvalidArgumentException — если кука собрана некорректно; список случаев — в разделе «Что не соберётся».

Пример

php
Cookie::add(Cookie::make('sid', $token)->expiresIn(3600));

Cookie::forget()

Просит браузер удалить куку.

Отдельной команды «удалить куку» в HTTP нет: удаление — это отправка куки с пустым значением и сроком жизни в прошлом. Метод делает именно это, избавляя от необходимости собирать такую куку вручную.

Синтаксис

php
public static function forget(string $name, string $path = '/', ?string $domain = null): void

Параметры

$name — имя удаляемой куки.

$path — путь, с которым кука была поставлена. По умолчанию /.

$domain — домен, с которым кука была поставлена. По умолчанию null, то есть кука без домена.

Пример

php
Cookie::forget('sid');
Cookie::forget('admin_pref', '/admin');

Путь и домен обязаны совпасть с теми, что были при установке

Для браузера путь и домен — часть личности куки, а не дополнение к ней. Кука, поставленная на /admin, не удаляется удалением на /: браузер видит запрос на удаление другой куки, а исходная продолжает жить и присылаться.

Это самая частая причина жалоб «выход из системы не работает».

Cookie::defaults()

Задаёт атрибуты, с которыми будет создаваться каждая кука из Cookie::make().

Настраивается один раз при старте приложения. Принимает не готовый образец, а функцию, и разница существенная: функция выполняется после подстановки Secure по схеме запроса, поэтому приложение может перекрыть и её — например, когда TLS терминируется на прокси, а до приложения запрос доходит уже по обычному HTTP.

Синтаксис

php
public static function defaults(?Closure $configure): void

Параметры

$configure — функция, принимающая SetCookie и возвращающая SetCookie. Значение null сбрасывает ранее заданные умолчания.

Пример

main/Application.php
use Flytachi\Winter\Kernel\Http\Cookie\{Cookie, SameSite, SetCookie};

protected static function configure(ApplicationArguments $args): void
{
  Cookie::defaults(fn(SetCookie $cookie) => $cookie
      ->domain('example.com')
      ->sameSite(SameSite::Strict));
}

Примечание

На SetCookie::make() умолчания не действуют — та версия остаётся чистой и ничего не знает ни о запросе, ни о настройках.

Cookie::init() и Cookie::clear()

Служебные методы жизненного цикла. Прикладной код их не вызывает.

init() вызывается роутером в начале каждого запроса: разбирает присланные куки и запоминает ответ, в который будут писаться отправляемые. clear() сбрасывает это состояние — нужен между запросами в режиме «один запрос на процесс» и в тестах.

php
public static function init(HttpRequest $request, HttpResponse $response): void
public static function clear(): void

SetCookie

SetCookie — описание одной куки: имя, значение и все атрибуты, с которыми она уйдёт к браузеру.

Объект неизменяемый: каждый метод настройки возвращает новый экземпляр, а исходный остаётся прежним. Благодаря этому заготовку можно держать в одном месте и раздавать, не опасаясь, что её испортят из другого:

php
$base = SetCookie::make('sid')->secure()->httpOnly()->expiresIn(3600);

$forFirstUser  = $base->value($firstToken);    // $base не изменился
$forSecondUser = $base->value($secondToken);

Умолчания подобраны безопасными:

Атрибут Значение Почему так
Path / кука видна всему сайту
HttpOnly включён скрипты на странице её не прочитают
SameSite Lax то же, что подставляют современные браузеры
Срок жизни сессия кука умирает вместе с окном браузера
Secure выключен см. ниже

Secure в умолчания не входит намеренно. Объект-значение не видит схему запроса, а кука с Secure, отправленная по обычному HTTP, браузером молча выбрасывается. Схему подставляет Cookie::make() — у него есть живой запрос.

SetCookie::make()

Создаёт описание куки без оглядки на запрос и настройки приложения.

Нужен там, где запроса нет: в тестах, в фоновой задаче, при сборке заготовки на старте. В контроллере обычно используют Cookie::make().

Синтаксис

php
public static function make(string $name, string $value = ''): SetCookie

Параметры

$name — имя куки.

$value — значение, по умолчанию пустое.

Ошибки

InvalidArgumentException — если имя пустое или содержит символ, недопустимый в имени куки: пробел, =, ;, ,, кавычку, скобку, слэш, управляющий символ.

Пример

php
use Flytachi\Winter\Kernel\Http\Cookie\SetCookie;

$cookie = SetCookie::make('locale', 'ru');

SetCookie::forget()

Собирает куку, удаляющую другую куку с тем же именем.

То же, что делает Cookie::forget(), но возвращает объект вместо того, чтобы сразу отправить. Пригодится, когда удаление нужно приложить к конкретному ответу, а не к текущему запросу.

Синтаксис

php
public static function forget(string $name, string $path = '/', ?string $domain = null): SetCookie

Параметры

Совпадают с Cookie::forget(): имя, а также путь и домен, с которыми кука была поставлена.

Возвращает

Куку с пустым значением и сроком в прошлом.

Результат

text
sid=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/; HttpOnly; SameSite=Lax

Пример

php
return ResponseEntity::noContent()->cookie(SetCookie::forget('sid'));

expiresIn()

Задаёт срок жизни куки в секундах от текущего момента.

Самая частая форма: «час», «месяц», «год» естественно выражаются длительностью, а не датой.

Синтаксис

php
public function expiresIn(int $seconds): SetCookie

Параметры

$seconds — сколько кука должна прожить. Ноль или отрицательное значение означает удаление — на этом и построен SetCookie::forget().

Возвращает

Новый объект с заданным сроком.

Пример

php
Cookie::make('sid', $token)->expiresIn(3600);                 // час
Cookie::make('remember', $token)->expiresIn(60 * 60 * 24 * 30);  // месяц

Результат

text
sid=abc; Expires=Tue, 19 Aug 2025 11:40:00 GMT; Max-Age=3600; Path=/; HttpOnly; SameSite=Lax

Отправляются оба атрибута сразу, и это не избыточность: Max-Age понимают современные браузеры, Expires — самые старые, а при наличии обоих стандарт предписывает руководствоваться Max-Age. Пара безопасна, а не противоречива.

expiresAt()

Задаёт срок жизни куки конкретным моментом.

Нужен там, где важна дата, а не длительность: «до конца акции», «до полуночи», «до истечения подписки».

Синтаксис

php
public function expiresAt(DateTimeInterface|int $moment): SetCookie

Параметры

$moment — объект даты либо unix-метка времени.

Возвращает

Новый объект с заданным сроком.

Пример

php
Cookie::make('promo', 'summer-sale')
  ->expiresAt(new DateTimeImmutable('2026-09-01 00:00:00'));

Примечание

Момент в прошлом означает удаление куки. Отрицательный Max-Age при этом не отправляется — вместо него уходит Max-Age=0, поскольку отрицательное значение часть клиентов считает ошибкой разбора.

session()

Убирает срок жизни: кука живёт до закрытия браузера.

Это поведение по умолчанию, поэтому метод нужен в одном случае — когда срок был задан раньше по цепочке и его надо отменить.

Синтаксис

php
public function session(): SetCookie

Пример

php
$base = SetCookie::make('csrf')->expiresIn(3600);

// Для этой формы токен не должен переживать окно браузера.
$oneTab = $base->value($token)->session();

path()

Задаёт префикс URL, для которого браузер будет присылать куку.

Синтаксис

php
public function path(string $path): SetCookie

Параметры

$path — путь, по умолчанию /. Кука с путём /admin присылается для /admin и /admin/users, но не для /.

Возвращает

Новый объект с заданным путём.

Пример

php
Cookie::make('admin_sidebar', 'collapsed')->path('/admin');

Примечание

Сужение пути — не средство защиты: любая страница сайта всё равно может отправить запрос на /admin и кука уйдёт вместе с ним. Это способ не таскать лишние данные в каждом запросе.

domain()

Задаёт домен, для которого действует кука.

Синтаксис

php
public function domain(?string $domain): SetCookie

Параметры

$domain — домен. Значение null (умолчание) означает «только текущий хост, без поддоменов» — и это безопаснее: кука не уедет на соседний поддомен, которым может владеть другая команда. Указанный домен покрывает и его поддомены: example.com включает api.example.com.

Возвращает

Новый объект с заданным доменом.

Пример

php
// Кука должна работать и на example.com, и на api.example.com
Cookie::make('sid', $token)->domain('example.com');

Примечание

Ведущая точка (.example.com) — наследие старого стандарта. Современные браузеры её игнорируют, писать её не нужно.

httpOnly()

Запрещает или разрешает доступ к куке из JavaScript.

Включён по умолчанию. С ним кука не видна в document.cookie, а значит найденная на странице уязвимость с внедрением скрипта не сможет её выкрасть. Для токена сессии это обязательное условие.

Синтаксис

php
public function httpOnly(bool $httpOnly = true): SetCookie

Параметры

$httpOnly — false открывает куку скриптам.

Возвращает

Новый объект.

Пример

php
Cookie::make('sid', $token)->httpOnly();            // включён и так, но пусть будет явно
Cookie::make('theme', 'dark')->httpOnly(false);    // читает скрипт оформления

Примечание

Выключайте только для значения, которое действительно читает JavaScript самой страницы: тема, свёрнутая панель, номер шага мастера. Токен сессии таким значением не бывает.

secure()

Требует, чтобы кука передавалась только по HTTPS.

Синтаксис

php
public function secure(bool $secure = true): SetCookie

Параметры

$secure — false снимает требование.

Возвращает

Новый объект.

Пример

php
SetCookie::make('sid', $token)->secure();

Примечание

При создании через Cookie::make() этот атрибут уже проставлен, если запрос пришёл по HTTPS. Вызывать вручную нужно в двух случаях: когда куку собирают через SetCookie::make(), и когда TLS терминируется на прокси, а до приложения запрос доходит по обычному HTTP.

sameSite()

Определяет, будет ли браузер прикладывать куку к запросам, инициированным другим сайтом.

Это основная защита от подделки межсайтовых запросов. Без неё браузер отправит вашу куку вместе с запросом, который сформировала чужая страница, и сервер не отличит его от настоящего действия пользователя.

Синтаксис

php
public function sameSite(?SameSite $sameSite): SetCookie

Параметры

$sameSite — вариант перечисления SameSite, либо null, чтобы не отправлять атрибут вовсе и оставить решение браузеру.

Возвращает

Новый объект.

Значения

Значение Когда браузер приложит куку Для чего подходит
SameSite::Lax свои запросы плюс переходы по ссылке сессия обычного сайта; значение по умолчанию
SameSite::Strict только свои запросы банк, админка. Переход по ссылке извне выглядит как «не вошёл»
SameSite::None всегда, включая чужие сайты виджет, встроенный в чужую страницу. Требует Secure
null атрибут не отправляется решает браузер; сегодня это то же, что Lax

Пример

php
use Flytachi\Winter\Kernel\Http\Cookie\SameSite;

Cookie::make('sid', $token)->sameSite(SameSite::Strict);

partitioned()

Помещает куку в отдельное хранилище для каждого сайта, в который встроена страница.

Механизм называется CHIPS и нужен виджетам. Если ваш виджет встроен и в a.example, и в b.example, без этого атрибута он получает одну общую куку и может по ней связать пользователя между сайтами. С ним у каждого встраивающего сайта своя.

Синтаксис

php
public function partitioned(bool $partitioned = true): SetCookie

Параметры

$partitioned — false снимает атрибут.

Возвращает

Новый объект.

Пример

php
Cookie::make('widget_state', $state)
  ->secure()
  ->sameSite(SameSite::None)
  ->partitioned();

Примечание

Требует Secure. Кука с Partitioned, но без него, собрана не будет — см. отказы.

raw()

Отправляет значение как есть, без кодирования.

По умолчанию значение кодируется: a b/c уезжает как a%20b%2Fc и возвращается обратно раскодированным. Отключать это стоит для значений, которые и так состоят из безопасных символов, — JWT, шестнадцатеричная подпись, идентификатор, — чтобы промежуточное звено не закодировало их второй раз.

Синтаксис

php
public function raw(bool $raw = true): SetCookie

Параметры

$raw — false возвращает кодирование.

Возвращает

Новый объект.

Ошибки

Сырое значение проверяется при отправке: пробел, кавычка, запятая, точка с запятой и управляющие символы приведут к InvalidArgumentException. Без этой проверки точка с запятой оборвала бы куку, а всё, что за ней, браузер прочитал бы как атрибуты.

Пример

php
Cookie::make('token', $jwt)->raw();

value()

Заменяет значение, сохраняя все атрибуты.

Метод существует ради заготовок: настроить один раз набор атрибутов и дальше выдавать по нему куки с разными значениями.

Синтаксис

php
public function value(string $value): SetCookie

Параметры

$value — новое значение.

Возвращает

Новый объект с тем же набором атрибутов.

Пример

php
$sessionCookie = SetCookie::make('sid')
  ->secure()
  ->httpOnly()
  ->sameSite(SameSite::Strict)
  ->expiresIn(3600);

Cookie::add($sessionCookie->value($token));

Чтение атрибутов

Одиннадцать методов, возвращающих то, что в объекте уже задано. Прикладному коду они обычно не нужны — используются в тестах и там, где куку перед отправкой проверяют.

php
public function getName(): string
public function getValue(): string
public function getPath(): string
public function getDomain(): ?string
public function getSameSite(): ?SameSite
public function getExpires(): ?int      // unix-метка, если срок задан моментом
public function getMaxAge(): ?int       // секунды, если срок задан длительностью
public function isSecure(): bool
public function isHttpOnly(): bool
public function isPartitioned(): bool
public function isRaw(): bool

Пример

php
$cookie = Cookie::make('sid', $token)->expiresIn(3600);

if (!$cookie->isSecure() && $environment === 'production') {
  throw new RuntimeException('В продакшене сессионная кука обязана быть Secure');
}

toHeader()

Собирает значение заголовка Set-Cookie — то, что уйдёт в ответ.

Вызывать вручную не нужно: это делает ответ при отправке. Метод полезен в тестах, где проверяют получившиеся байты, и при отладке.

Синтаксис

php
public function toHeader(?int $now = null): string

Параметры

$now — точка отсчёта для перевода длительности в дату и обратно. По умолчанию текущее время; в тестах передают фиксированное, чтобы результат не зависел от момента запуска.

Возвращает

Строку заголовка без его имени.

Ошибки

InvalidArgumentException — если кука собрана некорректно. Проверка выполняется именно здесь, а не в сеттерах: атрибуты задаются по одному, и порядок ->sameSite(None)->secure() так же законен, как обратный.

Пример

php
$header = SetCookie::make('sid', 'abc')
  ->expiresIn(3600)
  ->secure()
  ->toHeader(now: 1755600000);

Результат

text
sid=abc; Expires=Tue, 19 Aug 2025 11:40:00 GMT; Max-Age=3600; Path=/; Secure; HttpOnly; SameSite=Lax

SameSite

Перечисление значений атрибута SameSite. Три варианта, разобранных выше.

php
enum SameSite: string
{
  case Lax    = 'Lax';
  case Strict = 'Strict';
  case None   = 'None';
}

Что не соберётся

Объект отказывается отдавать куку, которую браузер всё равно молча выбросил бы. Проверка происходит при сборке заголовка, то есть при отправке.

Ситуация Что произойдёт
SameSite::None без Secure InvalidArgumentException — браузер такую куку отвергает
Partitioned без Secure InvalidArgumentException
raw() со значением, содержащим пробел, ;, , или кавычку InvalidArgumentException
Имя с пробелом, =, ;, ,, скобками, слэшем InvalidArgumentException при создании
Пустое имя InvalidArgumentException при создании
text
Cookie 'sid': SameSite=None requires Secure, or the browser discards the cookie.

Смысл проверок в том, что все перечисленные ошибки тихие: без них кука просто не появилась бы у клиента, а разбираться пришлось бы в браузере, а не в коде.


Чтение через объект запроса

Куки доступны и напрямую у запроса, если он уже инжектирован в метод контроллера. Пара методов названа по образцу заголовков: кука читается так же, как заголовок.

php
public function getCookie(string $name): ?string
public function getCookies(): array

Пример

php
#[GetMapping('me')]
public function me(HttpRequest $request): ResponseEntity
{
  $token = $request->getCookie('sid');

  return ResponseEntity::ok(['authenticated' => $token !== null]);
}

Почему возвращается карта строк, а не объекты

В библиотеках других языков этот метод иногда отдаёт массив объектов-кук. Здесь этого намеренно нет: во входящем запросе браузер присылает только пары «имя=значение» — ни срока жизни, ни пути, ни домена в нём нет.

Объекты с пустыми полями создавали бы впечатление, что атрибуты присланной куки можно прочитать. Карта строк не обещает того, чего в запросе не бывает. Атрибуты — это SetCookie, то есть исходящая сторона.


Как это устроено внутри

Раздел для любопытных: знать это, чтобы пользоваться куками, не требуется.

Чтение идёт из сырого заголовка, а не из встроенного разбора PHP. У PHP есть готовый массив кук, но он переименовывает имена. Один и тот же запрос:

text
Cookie: my.sid=1; my sid=2; ok=3

встроенный разбор PHP  →  ["my_sid", "ok"]     точка заменена, вторая кука выброшена
Winter                 →  ["my.sid", "my sid", "ok"]

Swoole разбирает заголовок сам и так не поступает — то есть на встроенном разборе два режима работы фреймворка давали бы разные наборы имён, и приложение, работающее в одном, ломалось бы в другом. Поэтому заголовок разбирается собственным кодом одинаково в обоих.

Всё остальное поведение намеренно повторяет привычное: из двух кук с одним именем побеждает первая, пустое значение сохраняется, имя без = читается как пустая строка.

Запись собирает заголовок сама. Средства выпуска кук, встроенные в PHP и в Swoole, пишут атрибуты по-разному — разным регистром и с разным кодированием пробела. Строку собирает toHeader(), и оба режима отправляют одни и те же байты, а тесты, проверяющие эти байты, что-то значат.

Дальше

  • Ответы — метод cookie() на любом типе ответа
  • Запросы — что ещё приходит вместе с куками
  • Middleware — удобное место, чтобы открыть или погасить сессию
  • Локализация — язык, выбранный пользователем, хранится в куке