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

Кто что ставит

На исходящем запросе заголовки делятся на три группы.

1. Ставим мы. Это полезная нагрузка для бэкенда: Content-TypeAcceptAuthorization, кастомные X-*.

2. Ставит браузер автоматически. Технические и часть служебных: HostUser-AgentContent-LengthConnectionCookieOriginReferer и другие. Их перечислять руками не надо, и попытка перезаписать Host или Cookie ничего не даст — браузер их не пустит ради безопасности.

3. «Forbidden header names». Список заголовков, которые из JS поставить нельзя в принципе. К нему относятся OriginCookieSet-CookieDateConnection и ряд других. Это сделано, чтобы JS не подделывал источник запроса.

Главные четыре

Content-Type

В исходящем запросе говорит серверу, в каком формате тело. На POST/PUT/PATCH с JSON всегда application/json. На загрузке файла через FormData — не ставим, браузер сам.

В ответе от сервера говорит, в каком формате тело пришло. Чаще всего application/json; charset=utf-8.

Accept

Говорит серверу, в каком формате клиент хочет получить ответ:

fetch('/api/posts', {
  headers: {
    'Accept': 'application/json',
  },
});

Если сервер умеет читать несколько форматов (JSON, XML, CSV) — он посмотрит на Accept и выберет подходящий. В REST API в 99% этот заголовок — application/json; в большинстве случаев его можно опустить, потому что сервер и так отдаёт JSON по умолчанию.

Authorization

Идентификация клиента. Несколько схем — в зависимости от того, что требует API. Все подробно — в модуле 7, здесь только формат:

// Bearer-токен (самый частый случай):
headers: { 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...' }

// Basic auth (логин:пароль в base64):
headers: { 'Authorization': 'Basic ' + btoa('user:password') }

// API key через кастомный заголовок (вариант некоторых сервисов):
headers: { 'X-API-Key': 'abc123...' }

Кастомные X-заголовки

Заголовки с префиксом X- исторически использовались для всего нестандартного. RFC 6648 рекомендовал перестать ставить такой префикс — но в индустрии X-* по-прежнему встречается на каждом шагу:

  • X-Request-ID — уникальный id запроса для трассировки в логах;
  • X-API-Key — альтернатива Authorization для простых API;
  • X-Client-Version — версия клиента, чтобы серверу было удобно дебажить;
  • X-CSRF-Token — защита от CSRF-атак в форменных API.

Ставим, если API в документации просит. Свои выдумывать без необходимости — не надо.

Как заголовки передаются в fetch

Самый частый способ — передать объект:

fetch(url, {
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
  },
});

Альтернативно можно передать массив пар или объект Headers:

const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.append('X-Custom', 'value');

fetch(url, { headers });

Имена заголовков HTTP не чувствительны к регистру: Content-Type и content-type — одно и то же. Объект Headers нормализует их сам.

Чтение заголовков ответа

В response.headers — объект Headers с заголовками ответа. У него есть .get(name).has(name), итерация:

const response = await fetch('/api/posts');
const contentType = response.headers.get('Content-Type');
const remaining  = response.headers.get('X-RateLimit-Remaining');

for (const [name, value] of response.headers) {
  console.log(name, value);
}

Имена при чтении тоже нечувствительны к регистру.

CORS-нюанс с чтением заголовков

Когда запрос идёт на другой домен (cross-origin), JS может прочитать ответные заголовки только из «безопасного» списка: Content-TypeCache-Control, ещё несколько. Все остальные — в том числе AuthorizationX-* — доступны только если сервер явно их разрешил через Access-Control-Expose-Headers. Подробно про CORS — в модуле 8.

В итоге

Из всего зоопарка заголовков фронтенд в повседневной работе руками ставит две-три штуки: Content-Type на POST/PUT/PATCH с JSON, Authorization на запросы к защищённым эндпоинтам, иногда кастомные X-*, если их требует конкретный API. Остальное либо ставит браузер, либо нам всё равно.