Learning Platform
Глоссарий Troubleshooting
Урок 03.05 · 17 мин
Начальный
HTTPHeadersCookiesContent-TypeAccept

HTTP-заголовки — метаинформация запроса и ответа

Заголовки в HTTP — это пары «ключ-значение», которые несут метаинформацию о запросе или ответе. Они определяют формат тела, аутентификацию, кэширование, кодировку, и десятки других вещей. Junior, который читает заголовки, отлаживает API за минуты. Junior, который их игнорирует, тратит часы и пишет в Stack Overflow вопросы вроде «почему мой POST возвращает 415».

В этом уроке разберём базовый набор заголовков, без которых HTTP не работает, плюс несколько специфических для DE.


Базовые свойства заголовков

Прежде чем разбирать конкретные — несколько технических деталей:

Свойства HTTP-заголовков
Имена case-insensitiveContent-Type, content-type, CONTENT-TYPE -- один и тот же заголовок. RFC 9110. Конвенция -- Pascal-Case-Через-Дефис
Значения case-sensitiveBearer abc123 != bearer abc123 для большинства серверов. Технически зависит от заголовка, но в общем случае -- да
Один заголовок -- много разSet-Cookie часто несколько штук в ответе. Accept-Encoding можно перечислить через запятую: gzip, deflate, br
Только ASCIIВ заголовках только ASCII-символы. Для не-ASCII значений -- RFC 8187 percent-encoding или явно указанный charset
Свои X-* заголовкиИсторически кастомные заголовки начинались с X-. RFC 6648 (2012) рекомендовал отказаться от X-, но de facto X- всё ещё распространены
HTTP/2 -- строчныеВ HTTP/2 заголовки обязательно в lowercase. Это требование протокола, не стиль

Заголовки запроса

Что Junior шлёт на сервер. Вот основные:

Host — куда идём

Host — единственный обязательный заголовок в HTTP/1.1. Без него запрос невалиден.

GET /api/users HTTP/1.1
Host: api.example.com

Зачем: один сервер (один IP-адрес) может обслуживать несколько доменов (api.example.com, www.example.com, admin.example.com). По заголовку Host сервер понимает, какой сайт вы запрашиваете. Это называется virtual hosting.

В HTTP/2 и HTTP/3 заголовок Host заменён на специальный pseudo-header :authority, но смысл тот же. curl и библиотеки сами добавляют его на основе URL.

Content-Type — что в теле запроса

Когда вы шлёте тело (POST/PUT/PATCH), сервер должен знать, как его парсить. Это говорится через Content-Type.

# JSON:
curl -X POST https://httpbin.org/post \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alice"}'

# Form-encoded (стандартная HTML-форма):
curl -X POST https://httpbin.org/post \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'name=Alice&age=30'

# Plain text:
curl -X POST https://httpbin.org/post \
  -H 'Content-Type: text/plain' \
  -d 'just some text'
WARNING

Если не указать Content-Type явно, curl по умолчанию ставит application/x-www-form-urlencoded. Если ваш API ждёт JSON, а вы прислали без Content-Type — получите 415 Unsupported Media Type или 400 Bad Request. Это самая частая причина ‘не работает мой POST на ровном месте’.

Content-Length — размер тела

Размер тела в байтах. Curl и библиотеки сами вычисляют и добавляют:

POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 28

{"name":"Alice","age":30}

Если заявленный Content-Length не совпадает с реальным размером тела — сервер закроет соединение с ошибкой. Альтернатива — Transfer-Encoding: chunked для случаев, когда размер не известен заранее.

Accept — что готов принять в ответе

Accept говорит серверу, какие форматы ответа клиент понимает. Сервер выбирает подходящий (это называется content negotiation).

# Я хочу только JSON:
curl -H 'Accept: application/json' https://api.example.com/users/42

# Я хочу JSON, или XML если JSON нет:
curl -H 'Accept: application/json, application/xml;q=0.9' https://api.example.com/users/42

# Я приму что угодно:
curl -H 'Accept: */*' https://api.example.com/users/42

q=0.9 — это quality factor, приоритет. Максимум 1.0 (по умолчанию), минимум 0. Сервер выбирает наиболее предпочитаемый формат, который умеет.

В REST API обычно сервер всё равно отдаёт JSON, и Accept — формальность. Но если API поддерживает XML legacy-режим, или умеет CSV — Accept важен.

Accept-Encoding — компрессия

Какие алгоритмы сжатия поддерживает клиент. Сервер сжимает ответ — клиент разжимает. Экономия трафика 5-20x для текста (HTML, JSON).

Компрессия на лету: gzip, zstd и друзья
curl -v -H 'Accept-Encoding: gzip' https://api.github.com/users/torvalds 2>&1 | grep -E 'content-encoding|content-length'
# > Accept-Encoding: gzip
# < content-encoding: gzip
# < content-length: 1234   <- сжатый размер. Без gzip было бы 4-5K

Распространённые алгоритмы:

  • gzip — старый, везде поддерживается
  • deflate — почти gzip, но другая обёртка
  • br (Brotli) — современный, лучше сжимает (на ~20% лучше gzip), Chrome+Firefox+Safari поддерживают
  • zstd — самый новый (Facebook), пока редко в API

Curl автоматически разжимает ответ, если поставить флаг --compressed:

curl --compressed https://api.github.com/users/torvalds
# Шлёт Accept-Encoding: gzip, deflate, br и сам разжимает

Authorization — кто я

Главный заголовок аутентификации. Подробно — модуль 8. Базово:

# Bearer token (OAuth2, JWT):
curl -H 'Authorization: Bearer ghp_abc123def456...' https://api.github.com/user

# Basic auth (login:password в base64):
curl -H 'Authorization: Basic dXNlcjpwYXNz' https://httpbin.org/basic-auth/user/pass
# или эквивалентно:
curl -u user:pass https://httpbin.org/basic-auth/user/pass

# API key (часто кастомные заголовки, не Authorization):
curl -H 'X-API-Key: my_secret_key' https://api.example.com/data

User-Agent — кто этот клиент

Идентификация клиента: имя, версия, иногда платформа.

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ...
User-Agent: curl/8.4.0
User-Agent: python-requests/2.34.0
User-Agent: MyETLBot/1.0 ([email protected])

Многие API логируют User-Agent для аналитики. Некоторые (GitHub, Reddit) требуют осмысленный User-Agent — без него возвращают 403. Хорошая практика для своих ETL — поставить узнаваемое имя плюс контакт:

import requests

session = requests.Session()
session.headers.update({
    'User-Agent': 'MyCompany-ETL/1.0 ([email protected])',
})

Если что-то сломается — owner API сможет связаться с вами.


Заголовки ответа

Что сервер шлёт обратно. Многие пересекаются с request, но есть и специфические.

Content-Type — что в теле ответа

Тот же заголовок, что и в запросе, но теперь говорит о теле ответа.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"name":"torvalds"}

charset=utf-8 — параметр заголовка. Указывает кодировку текста. Для JSON по умолчанию UTF-8 (RFC 8259), но многие явно указывают.

Content-Length и Transfer-Encoding

Размер ответа. Один из них обязателен (иначе клиент не знает, когда остановиться):

  • Content-Length: 1234 — известный размер.
  • Transfer-Encoding: chunked — потоковая передача, размер заранее неизвестен (например, генерируется в процессе).

Date — время сервера

Время на сервере в момент ответа, в формате RFC 7231:

Date: Thu, 14 May 2026 12:34:56 GMT

Полезно для отладки расхождений по времени и для кэширования (сравнить с Last-Modified).

Server — что за сервер

Идентификация сервера. Часто скрывают (security through obscurity):

Server: nginx/1.25.0
Server: gunicorn/19.9.0
Server: cloudflare

Cache-Control — как кэшировать

Главный заголовок управления кэшированием. Подробно — модуль 3.

Cache-Control: no-cache              # каждый раз проверять у сервера
Cache-Control: no-store              # вообще не кэшировать (sensitive data)
Cache-Control: max-age=3600          # кэшировать 1 час
Cache-Control: public, max-age=86400 # публичный кэш, 1 день
Cache-Control: private, max-age=300  # только в браузере, 5 минут

Location — куда идти дальше

Используется в редиректах (3xx) и при создании ресурса (201):

HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-url

HTTP/1.1 201 Created
Location: /api/users/42

ETag — версия ресурса

Уникальный идентификатор текущей версии ресурса. Используется для conditional requests и кэширования. Подробно — модуль 3.

ETag: "abc123def456"

# Клиент шлёт его обратно для проверки:
# If-None-Match: "abc123def456"
# Сервер ответит 304 Not Modified, если ресурс не изменился

Cookie — механизм, который позволяет серверу хранить данные у клиента и получать их обратно на каждом запросе. Основа большинства веб-сессий.

Жизненный цикл cookie
ЛогинPOST /login с username/password
Set-CookieСервер в ответе: Set-Cookie: session=abc123; HttpOnly; Secure; Max-Age=3600
Клиент сохраняетБраузер кладёт cookie в свой storage. requests.Session() -- в session.cookies
CookieНа каждом следующем запросе: Cookie: session=abc123. Клиент САМ шлёт обратно

Важные атрибуты Set-Cookie:

  • HttpOnly — JavaScript не может прочитать (защита от XSS).
  • Secure — отправляется только по HTTPS.
  • SameSite=Strict|Lax|None — когда отправлять на cross-site запросы (защита от CSRF).
  • Max-Age=3600 — TTL в секундах. Без него — session cookie (живёт до закрытия браузера).
  • Expires=... — старый формат TTL, через дату.
  • Domain=...; Path=... — на какие URL отправлять.
# Получить cookie:
curl -i https://httpbin.org/cookies/set?session=abc123
# < Set-Cookie: session=abc123; Path=/

# Отправить cookie вручную:
curl -H 'Cookie: session=abc123' https://httpbin.org/cookies
# {"cookies": {"session": "abc123"}}

# С --cookie-jar curl сохраняет cookies в файл:
curl -c cookies.txt https://httpbin.org/cookies/set?session=abc123
# А потом отдаёт обратно через -b:
curl -b cookies.txt https://httpbin.org/cookies

В Python requests.Session() автоматически работает с cookies — сохраняет из ответов и шлёт обратно. Подробно — модуль 6.

import requests

s = requests.Session()
s.get('https://httpbin.org/cookies/set?session=abc123')
# Cookie сохранился в s.cookies

response = s.get('https://httpbin.org/cookies')
print(response.json())  # {"cookies": {"session": "abc123"}}

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

curl -v, --resolve, --trace: глубокая диагностика API

API часто добавляют свои заголовки для специфической функциональности. Исторически — с префиксом X-. RFC 6648 (2012) рекомендовал отказаться от X-, но de facto префикс остался.

Типичные кастомные заголовки в API
X-API-KeyКастомный заголовок для API key auth (вместо Authorization). Используют OpenWeather, NewsAPI, многие другие
X-Request-IdUUID запроса. Сервер логирует под этим ID. Полезно при поддержке: 'у меня X-Request-Id 12345 был, гляньте что случилось'
X-RateLimit-*X-RateLimit-Limit, -Remaining, -Reset. Информация о rate limits. Многие API возвращают на каждом ответе
X-Total-CountОбщее количество элементов в коллекции (для pagination). GitHub возвращает X-Total-Count: 1234
Idempotency-KeyБЕЗ X-! UUID для дедупликации POST. Stripe, GitHub, AWS используют
X-Forwarded-ForРеальный IP клиента (когда между клиентом и сервером есть прокси). Заголовок добавляет прокси

Юзайте кастомные заголовки осознанно. Не выдумывайте X-My-Internal-Flag, если хватает body. Они полезны для метаданных, которые не вписываются в payload.


Как смотреть заголовки в реальной работе

Несколько полезных команд:

# Только заголовки ответа (HEAD):
curl -I https://api.github.com/users/torvalds

# Заголовки запроса и ответа (verbose):
curl -v https://api.github.com/users/torvalds

# Конкретный заголовок:
curl -sI https://api.github.com/users/torvalds | grep -i 'content-type'

# В Python с requests:
# >>> response.headers -- case-insensitive dict
# >>> response.headers['content-type']
# >>> response.headers.get('x-ratelimit-remaining')

В curl флаг -i показывает заголовки ответа в составе вывода (но не верхние request headers, для них нужен -v).


Попробуй сам

Поэкспериментируйте с заголовками:

# 1. Httpbin echo -- увидеть, какие заголовки ваш клиент шлёт по умолчанию:
curl https://httpbin.org/headers
# Вернёт JSON со всеми вашими headers, включая User-Agent, Accept

# 2. Те же заголовки, но добавим свои:
curl -H 'X-Custom-Header: my-value' \
     -H 'Accept: application/json' \
     -H 'User-Agent: MyClient/1.0' \
     https://httpbin.org/headers

# 3. POST с разными Content-Type:
curl -X POST https://httpbin.org/post \
     -H 'Content-Type: application/json' \
     -d '{"key":"value"}'

curl -X POST https://httpbin.org/post \
     -H 'Content-Type: application/x-www-form-urlencoded' \
     -d 'key=value'

# Сравните, как httpbin парсит -- поле "json" vs "form" в ответе

# 4. Проверить gzip-компрессию:
curl --compressed -I https://api.github.com/users/torvalds
# < Content-Encoding: gzip

# 5. Cookie:
curl -c cookies.txt -b cookies.txt \
     https://httpbin.org/cookies/set?session=test123
# В cookies.txt появится файл с cookie. Откройте текстовым редактором -- увидите формат

Проверка знанийKnowledge check
Junior шлёт POST на API: 'curl -X POST https://api.example.com/users -d \'{"name":"Alice"}\''. Сервер возвращает 415 Unsupported Media Type. Что забыл и почему 415?
ОтветAnswer
Junior забыл указать Content-Type. По умолчанию curl при -d ставит Content-Type: application/x-www-form-urlencoded (как HTML-форма), а не application/json. Сервер ожидает JSON (это REST API), получает Content-Type form-encoded и пытается интерпретировать тело как форму. Тело {"name":"Alice"} как форма выглядит бессмысленно -- там нет '=' и '&'. Сервер возвращает 415 Unsupported Media Type, что означает 'я не поддерживаю Content-Type, который ты прислал, для этого endpoint'. Решение: добавить -H 'Content-Type: application/json' к команде. Полная правильная команда: curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -d '{"name":"Alice"}'. В Python с requests эта проблема обычно не встречается -- если использовать json= вместо data=, requests сам ставит Content-Type: application/json: requests.post(url, json={"name":"Alice"}). А вот requests.post(url, data='{"name":"Alice"}') -- снова форма, снова 415. Это одна из самых частых ошибок Junior, и теперь вы знаете, как её избежать. Бонус: иногда сервер вместо 415 возвращает 400 Bad Request с messageом 'Cannot parse body' -- функционально та же проблема, разные API по-разному реагируют.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Какой заголовок ОБЯЗАТЕЛЬНЫЙ в каждом HTTP/1.1 запросе?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5