HTTP-заголовки
Разберём что такое заголовки, какими они бывают и как работают заголовки авторизации, кеширования и безопасности.
Зачем нужны заголовки
В первой главе мы разобрали что HTTP-сообщение состоит из трёх частей: стартовая строка, заголовки, тело. Стартовая строка говорит что делать и с чем. Тело несёт данные. А заголовки — это инструкции и мета-информация о запросе или ответе.
Формат всегда один и тот же — Имя: Значение, по одному на строку:
Host: api.example.com Content-Type: application/json Authorization: Bearer eyJhbGci...
Заголовки делятся на три группы:
- Заголовки запроса — клиент сообщает серверу что-то о себе или о запросе
- Заголовки ответа — сервер сообщает клиенту что-то об ответе или о себе
- Общие — могут присутствовать и в запросе, и в ответе
Заголовки запроса
Host
Host: api.example.com
Самый важный заголовок. Указывает на какой именно сайт адресован запрос.
На одном IP-адресе может висеть десятки сайтов. Когда запрос приходит на веб-сервер,
именно заголовок Host помогает понять какому сайту он адресован.
Host не был обязательным — тогда на одном IP висел один сайт
и серверу не нужно было выбирать. В HTTP/1.1 Host стал обязательным именно потому
что появился виртуальный хостинг: несколько сайтов на одном IP, и сервер должен понимать
к какому из них обращается клиент.
Content-Type
Content-Type: application/json Content-Type: multipart/form-data; boundary=----FormBoundary Content-Type: application/x-www-form-urlencoded
Говорит серверу в каком формате передано тело запроса. Без этого заголовка сервер не знает как парсить тело — воспринимает его как набор байт.
| Значение | Когда используется |
|---|---|
| application/json | REST API, данные в формате JSON |
| application/x-www-form-urlencoded | Обычные HTML-формы |
| multipart/form-data | Загрузка файлов |
| text/plain | Простой текст |
| application/xml | XML-данные |
Host: api.example.com
Content-Type: application/json
{
"name": "Алексей",
"email": "alex@mail.ru"
}
Тело — JSON, парси соответственно»
Content-Type: application/json → запускает JSON-парсер → получает объект → сохраняет в БД400 Bad RequestAuthorization
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... Authorization: Basic dXNlcjpwYXNzd29yZA== Authorization: ApiKey my-secret-key
Передаёт учётные данные для аутентификации. Схем несколько:
Bearer — самая распространённая в современных API. После слова Bearer идёт токен — обычно JWT.
Basic — логин и пароль в Base64. Важно: Base64 — это не шифрование, это просто кодировка. dXNlcjpwYXNzd29yZA== легко декодируется обратно в user:password. Поэтому Basic допустим только по HTTPS.
Как токен появляется у пользователя
{"login": "alex", "password": "qwerty"}
{"token": "eyJhbGci..."}
(в памяти, localStorage, cookie)
GET /api/orders
Authorization: Bearer eyJhbGci...
Авторизация — что тебе можно? Сервер проверяет токен и определяет права.
Заголовок называется
Authorization но по токену сервер делает сразу обе проверки.
Accept
Accept: application/json Accept: text/html, application/json Accept: */*
Говорит серверу в каком формате клиент хочет получить ответ. */* — любой формат. Браузеры обычно отправляют длинный список — все форматы которые умеют отображать.
Accept-Encoding
Accept-Encoding: gzip, deflate, br
Говорит серверу какие алгоритмы сжатия клиент умеет распаковывать. Сервер выбирает алгоритм и отдаёт сжатое тело — а в ответе указывает Content-Encoding каким алгоритмом сжал.
| Алгоритм | Описание |
|---|---|
| gzip | Стандарт де-факто, поддерживается везде |
| deflate | Устаревший, почти не используется |
| br (Brotli) | Современный, жмёт лучше gzip на 15–25%, поддерживается всеми актуальными браузерами |
Зачем это нужно: типичная HTML-страница или JSON-ответ API сжимается в 3–5 раз. Вместо 100 КБ по сети летит 25 КБ — быстрее загрузка, меньше трафик. Если клиент не отправил Accept-Encoding — сервер отдаёт тело без сжатия.
User-Agent
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0 User-Agent: curl/8.1.2 User-Agent: python-requests/2.31.0
Идентификатор клиента — браузер, версия, ОС или название программы. Сервер может менять поведение в зависимости от клиента.
Referer
Referer: https://app.example.com/dashboard
Адрес страницы с которой пришёл запрос. Браузер добавляет его автоматически при переходе по ссылке или при загрузке ресурсов (картинок, скриптов). Полезен для аналитики и частичной защиты от CSRF.
Что такое CSRF — чужой сайт делает запрос от имени залогиненного пользователя без его ведома. Например: пользователь залогинен в банке, зашёл на вредоносный сайт — и тот незаметно отправил запрос на перевод денег от его имени.
Как помогает Referer — сервер смотрит откуда пришёл запрос. Если на mybank.com пришёл запрос на перевод денег, а Referer указывает на чужой сайт — это подозрительно:
Referrer-Policy).
Основная защита от CSRF — CSRF-токен: случайное значение которое сервер выдаёт клиенту
и проверяет при каждом запросе. Чужой сайт этот токен не знает и не может его прочитать — запрос не пройдёт.
X-CSRF-Token: a8f3d2c1...
Cookie
Cookie: session_id=abc123; theme=dark; lang=ru
Передаёт куки которые браузер сохранил ранее. Куки — пары ключ-значение которые сервер попросил
браузер запомнить (через заголовок Set-Cookie в ответе). При каждом следующем запросе
браузер возвращает их автоматически.
Используются для хранения сессий, мелких настроек интерфейса (тема, язык, регион), а также для авторизации — кука может
хранить токен аналогично заголовку Authorization. Разница в том что браузер добавляет
куку автоматически, тогда как Authorization приложение добавляет вручную.
Cookie vs Authorization
Кука и заголовок Authorization делают одно и то же — говорят серверу кто ты. Просто разными способами:
// Способ 1 — через куку (браузер добавляет сам): Cookie: token=eyJhbGci... // Способ 2 — через заголовок (JS добавляет вручную): Authorization: Bearer eyJhbGci...
Кука — браузер управляет ей автоматически. Удобно для обычных сайтов с HTML-страницами и формами.
Именно поэтому на большинстве сайтов в DevTools ты не увидишь заголовок Authorization — авторизация идёт через куку.
Authorization — появился для API и мобильных приложений. Мобильное приложение не имеет браузера который автоматически управляет куками, поэтому токен хранится в памяти приложения и добавляется в заголовок вручную при каждом запросе:
fetch('/api/orders', { headers: { 'Authorization': 'Bearer ' + token } })
API / мобильное приложение → чаще Authorization
Заголовки ответа
Content-Type
Content-Type: application/json; charset=utf-8 Content-Type: text/html; charset=utf-8 Content-Type: image/jpeg
Сервер говорит клиенту в каком формате отдаёт тело ответа. Браузер смотрит на этот заголовок чтобы понять — показать HTML, распарсить JSON, отобразить картинку или скачать файл. charset=utf-8 указывает кодировку.
Content-Length
Content-Length: 1842
Размер тела ответа в байтах. При стриминге (chunked transfer) этого заголовка нет — размер заранее неизвестен.
Location
Location: /api/users/43 Location: https://new.example.com/page
- При редиректах (3xx) — указывает куда перейти
- При создании ресурса (201 Created) — указывает URL созданного объекта
Set-Cookie
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=3600
Просит браузер сохранить куку.
| Параметр | Смысл |
|---|---|
| HttpOnly | JavaScript не может читать куку — защита от XSS |
| Secure | Кука передаётся только по HTTPS |
| SameSite=Strict | Кука не отправляется при кросс-сайтовых запросах — защита от CSRF |
| Max-Age=3600 | Время жизни в секундах |
| Domain=.example.com | На каких поддоменах действует |
Server
Server: nginx/1.24.0
Идентифицирует ПО веб-сервера и его версию.
Cache-Control
Cache-Control: max-age=3600 Cache-Control: no-cache, no-store Cache-Control: public, max-age=86400
Управляет кешированием ответа. Один из самых важных заголовков для производительности.
| Значение | Смысл |
|---|---|
| max-age=3600 | Кешировать на 3600 секунд |
| no-cache | Можно кешировать, но перед использованием проверять актуальность |
| no-store | Не кешировать вообще (чувствительные данные) |
| public | Можно кешировать на любом узле (CDN, прокси) |
| private | Кешировать только в браузере, не на CDN |
Content-Encoding
Content-Encoding: gzip Content-Encoding: br
Сообщает клиенту каким алгоритмом сжато тело ответа. Работает в паре с Accept-Encoding из запроса — клиент сказал что умеет, сервер выбрал и сообщает что выбрал:
# Запрос клиента: Accept-Encoding: gzip, br # Ответ сервера: Content-Encoding: gzip Content-Length: 24817 # размер уже сжатого тела [тело сжато gzip — браузер распакует автоматически]
Vary: Accept-Encoding говорит CDN и прокси: «один URL может иметь разное тело в зависимости от Accept-Encoding — кешируйте варианты отдельно».
Общие заголовки
Connection
Connection: keep-alive Connection: close
Управляет поведением TCP-соединения. keep-alive — соединение остаётся открытым для следующих запросов. close — закрыть после ответа.
Что такое keep-alive
Без keep-alive каждый HTTP-запрос требует открывать новое TCP-соединение, а если HTTPS — ещё и TLS handshake. Это дорого:
TLS handshake → ~150ms
Запрос/ответ → ~150ms
соединение закрывается
TCP handshake → ~150ms
TLS handshake → ~150ms
Запрос/ответ → ~150ms
и снова...
TLS handshake → ~150ms ← один раз
Запрос 1 → ~150ms
Запрос 2 → ~150ms ← без handshake
Запрос 3 → ~150ms ← без handshake
соединение остаётся открытым
В HTTP/1.1 keep-alive включён по умолчанию — браузер и сервер переиспользуют соединение автоматически. Но есть нюанс: запросы в одном соединении идут последовательно. Пока не пришёл ответ на первый — второй не отправляется. Это называется head-of-line blocking:
← 200 OK ← ждём
→ GET /style.css
← 200 OK ← ждём
→ GET /logo.png
← 200 OK
→ GET /style.css ├ все сразу
→ GET /logo.png ┘
← 200 OK (style.css)
← 200 OK (logo.png)
← 200 OK (index.html)
Transfer-Encoding
Transfer-Encoding: chunked
Тело передаётся частями (чанками) — когда общий размер неизвестен заранее. Типично для стриминга, Server-Sent Events, больших файлов. При chunked-кодировании заголовка Content-Length нет.
Content-Type: application/json
Content-Length: 1842
← размер известен до отправки
{"users": [...]}
Content-Type: text/html
Transfer-Encoding: chunked
← Content-Length отсутствует
7\r\nHello, \r\n
6\r\nWorld!\r\n
5\r\nDone.\r\n
0\r\n← конец передачи
Версии HTTP
В лекциях мы используем HTTP/1.1 для примеров — его текстовый формат наглядный и читаемый. Но на практике большинство сайтов уже работают по HTTP/2 или HTTP/3. Разберём три версии чтобы понять чем они отличаются и почему эволюционировали.
HTTP/1.1 (1997)
Текстовый протокол — запрос и ответ можно прочитать глазами. Именно поэтому мы используем его в примерах.
Главное ограничение — head-of-line blocking: в одном TCP-соединении запросы идут строго по очереди. Пока не пришёл ответ на первый — второй ждёт.
HTTP/2 (2015)
Бинарный протокол — человек не может прочитать, зато парсится быстрее. Ключевое изменение — мультиплексирование: несколько запросов идут параллельно в одном TCP-соединении, не дожидаясь ответов.
← 200 OK ← ждём
→ GET /style.css
← 200 OK ← ждём
→ GET /logo.png
← 200 OK
→ GET /style.css ├ все сразу
→ GET /logo.png ┘
← 200 OK (style.css)
← 200 OK (logo.png)
← 200 OK (index.html)
Дополнительно HTTP/2 сжимает заголовки (HPACK) — в HTTP/1.1 одни и те же заголовки (Host, Cookie, User-Agent) передаются полностью в каждом запросе. В HTTP/2 повторяющиеся заголовки передаются один раз, дальше — только ссылка на индекс.
Семантика HTTP не изменилась — те же методы, те же заголовки, те же коды ответа. Изменился только транспорт.
HTTP/3 (2022)
HTTP/2 решил head-of-line blocking на уровне HTTP, но проблема осталась уровнем ниже — в TCP. Если теряется один TCP-пакет, весь поток данных встаёт пока пакет не будет переотправлен. Все мультиплексированные потоки HTTP/2 ждут из-за одного потерянного пакета.
HTTP/3 заменил TCP на QUIC — протокол поверх UDP. Каждый HTTP-поток — независимый QUIC-поток: потеря пакета в одном не блокирует остальные. TLS 1.3 встроен прямо в QUIC — handshake быстрее, а при повторном подключении возможен 0-RTT (данные отправляются вместе с первым пакетом).
Сравнение
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Формат | текстовый | бинарный | бинарный |
| Транспорт | TCP | TCP | QUIC (UDP) |
| Запросы | последовательно | мультиплекс | мультиплекс |
| Соединений | 6–8 параллельно | 1 | 1 |
| TLS | опциональный | обязательный | встроен в QUIC |
Заголовки безопасности
Это заголовки ответа которые управляют поведением браузера. Выставляются на уровне веб-сервера и защищают от целого класса атак. Подробно разберём в отдельной теме по безопасности — здесь краткий обзор.
Strict-Transport-Security (HSTS)
Strict-Transport-Security: max-age=31536000; includeSubDomains
Говорит браузеру: «всегда обращайся ко мне только по HTTPS, даже если пользователь напишет http://». Браузер запоминает это на max-age секунд.
X-Frame-Options
X-Frame-Options: DENY X-Frame-Options: SAMEORIGIN
Запрещает встраивать страницу в <iframe> на чужих сайтах. Защита от clickjacking — когда злоумышленник накладывает прозрачный iframe поверх своей страницы и перехватывает клики пользователя.
X-Content-Type-Options
X-Content-Type-Options: nosniff
Запрещает браузеру угадывать тип контента. Без этого заголовка браузер может интерпретировать картинку как HTML с JavaScript внутри.
Content-Security-Policy (CSP)
Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com
Самый мощный заголовок безопасности. Говорит браузеру откуда разрешено загружать ресурсы — скрипты, стили, картинки, фреймы. Основная защита от XSS-атак.