Глава 2

HTTP-заголовки

Разберём что такое заголовки, какими они бывают и как работают заголовки авторизации, кеширования и безопасности.

📖 ~20 мин Основы

Зачем нужны заголовки

В первой главе мы разобрали что HTTP-сообщение состоит из трёх частей: стартовая строка, заголовки, тело. Стартовая строка говорит что делать и с чем. Тело несёт данные. А заголовки — это инструкции и мета-информация о запросе или ответе.

📦
Аналогия: наклейки на коробке Если тело запроса — посылка, то заголовки — наклейки на коробке. «Хрупкое», «не переворачивать», «от кого», «срок хранения». Посылка дойдёт и без наклеек, но получатель не будет знать как с ней обращаться.

Формат всегда один и тот же — Имя: Значение, по одному на строку:

HTTP Headers
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGci...

Заголовки делятся на три группы:

Host
Куда адресован запрос
запрос
Authorization
Токен или логин/пароль
запрос
Content-Type
Формат тела сообщения
оба
Cache-Control
Управление кешированием
ответ
Set-Cookie
Сохранить куку в браузере
ответ
Strict-Transport-Security
Только HTTPS
безопасность

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

Host

Header
Host: api.example.com

Самый важный заголовок. Указывает на какой именно сайт адресован запрос.

На одном IP-адресе может висеть десятки сайтов. Когда запрос приходит на веб-сервер, именно заголовок Host помогает понять какому сайту он адресован.

Один сайт на IP — Host не важен
🌐
Браузер
GET / HTTP/1.1 Host: site.com
🗄️
Веб-сервер
IP: 93.184.216.34
site.com
Запрос пришёл на этот IP — значит для этого сайта. Host не нужен.
VS
Несколько сайтов на IP — Host обязателен
🌐
Браузер
GET / HTTP/1.1 Host: shop.com
🗄️
Веб-сервер
IP: 93.184.216.34
blog.com
shop.com ← Host совпал
news.com
Сервер читает Host и направляет запрос нужному сайту.
ℹ️
HTTP/1.0 vs HTTP/1.1 В HTTP/1.0 заголовок Host не был обязательным — тогда на одном IP висел один сайт и серверу не нужно было выбирать. В HTTP/1.1 Host стал обязательным именно потому что появился виртуальный хостинг: несколько сайтов на одном IP, и сервер должен понимать к какому из них обращается клиент.

Content-Type

Header
Content-Type: application/json
Content-Type: multipart/form-data; boundary=----FormBoundary
Content-Type: application/x-www-form-urlencoded

Говорит серверу в каком формате передано тело запроса. Без этого заголовка сервер не знает как парсить тело — воспринимает его как набор байт.

ЗначениеКогда используется
application/jsonREST API, данные в формате JSON
application/x-www-form-urlencodedОбычные HTML-формы
multipart/form-dataЗагрузка файлов
text/plainПростой текст
application/xmlXML-данные
Как это работает
Браузер отправляет запрос
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "name": "Алексей",
  "email": "alex@mail.ru"
}
📤 «Добавь нового пользователя.
Тело — JSON, парси соответственно»
Сервер видит Content-Type и знает что делать
С заголовком
Читает Content-Type: application/json → запускает JSON-парсер → получает объект → сохраняет в БД
Без заголовка
Тело есть, но формат неизвестен → воспринимает как набор байт → не может распарсить → 400 Bad Request

Authorization

Header
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Authorization: Basic dXNlcjpwYXNzd29yZA==
Authorization: ApiKey my-secret-key

Передаёт учётные данные для аутентификации. Схем несколько:

Bearer — самая распространённая в современных API. После слова Bearer идёт токен — обычно JWT.

Basic — логин и пароль в Base64. Важно: Base64 — это не шифрование, это просто кодировка. dXNlcjpwYXNzd29yZA== легко декодируется обратно в user:password. Поэтому Basic допустим только по HTTPS.

Как токен появляется у пользователя

1
Пользователь вводит логин и пароль
2
POST /auth/login
{"login": "alex", "password": "qwerty"}
3
Сервер проверяет данные, генерирует токен:
{"token": "eyJhbGci..."}
4
Браузер сохраняет токен
(в памяти, localStorage, cookie)
5
Все следующие запросы идут с токеном:
GET /api/orders
Authorization: Bearer eyJhbGci...
6
Сервер проверяет токен — кто это и что ему можно
🪪
Аналогия: бейдж на проходной Один раз показал паспорт (логин/пароль), получил бейдж (токен), и дальше везде ходишь с бейджем — паспорт каждый раз не нужен.
💡
Аутентификация vs Авторизация Аутентификациякто ты? Проверка логина и пароля, выдача токена.
Авторизациячто тебе можно? Сервер проверяет токен и определяет права.
Заголовок называется Authorization но по токену сервер делает сразу обе проверки.

Accept

Header
Accept: application/json
Accept: text/html, application/json
Accept: */*

Говорит серверу в каком формате клиент хочет получить ответ. */* — любой формат. Браузеры обычно отправляют длинный список — все форматы которые умеют отображать.

Accept-Encoding

Header
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

Header
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

Идентификатор клиента — браузер, версия, ОС или название программы. Сервер может менять поведение в зависимости от клиента.

User-Agent
🌐
Браузер
Mozilla/5.0 Chrome/124
HTML Полная страница со стилями и скриптами
📱
Мобильный браузер
Mozilla/5.0 iPhone Safari
HTML mobile Мобильная версия сайта или редирект на m.site.com
⚙️
API-клиент
curl/8.1.2
JSON Чистые данные без HTML-обёртки
🤖
Поисковый бот
Googlebot/2.1
HTML Страница без авторизации, с SEO-метатегами

Referer

Header
Referer: https://app.example.com/dashboard

Адрес страницы с которой пришёл запрос. Браузер добавляет его автоматически при переходе по ссылке или при загрузке ресурсов (картинок, скриптов). Полезен для аналитики и частичной защиты от CSRF.

Что такое CSRF — чужой сайт делает запрос от имени залогиненного пользователя без его ведома. Например: пользователь залогинен в банке, зашёл на вредоносный сайт — и тот незаметно отправил запрос на перевод денег от его имени.

Как помогает Referer — сервер смотрит откуда пришёл запрос. Если на mybank.com пришёл запрос на перевод денег, а Referer указывает на чужой сайт — это подозрительно:

Referer: https://mybank.com/payment ✓ свой сайт, всё ок
Referer: https://evil.com ✗ чужой сайт, блокируем
Referer отсутствует ⚠ подозрительно
⚠️
Referer — ненадёжная защита Его можно подделать, а браузер может не отправить вовсе (режим инкогнито, политика Referrer-Policy). Основная защита от CSRF — CSRF-токен: случайное значение которое сервер выдаёт клиенту и проверяет при каждом запросе. Чужой сайт этот токен не знает и не может его прочитать — запрос не пройдёт.
CSRF-токен в заголовке
X-CSRF-Token: a8f3d2c1...

Cookie

Header
Cookie: session_id=abc123; theme=dark; lang=ru

Передаёт куки которые браузер сохранил ранее. Куки — пары ключ-значение которые сервер попросил браузер запомнить (через заголовок Set-Cookie в ответе). При каждом следующем запросе браузер возвращает их автоматически.

Используются для хранения сессий, мелких настроек интерфейса (тема, язык, регион), а также для авторизации — кука может хранить токен аналогично заголовку Authorization. Разница в том что браузер добавляет куку автоматически, тогда как Authorization приложение добавляет вручную.

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

Content-Type

Header
Content-Type: application/json; charset=utf-8
Content-Type: text/html; charset=utf-8
Content-Type: image/jpeg

Сервер говорит клиенту в каком формате отдаёт тело ответа. Браузер смотрит на этот заголовок чтобы понять — показать HTML, распарсить JSON, отобразить картинку или скачать файл. charset=utf-8 указывает кодировку.

Как Content-Type управляет поведением браузера
🌐
Браузер GET /index.html
Сервер Вот файл. Открывай как text/html
🗄️
🖥️ Браузер отрисовывает страницу
🌐
Браузер GET /api/users
Сервер Вот данные. Читай как application/json
🗄️
⚙️ Браузер парсит JSON и отдаёт JS-коду
🌐
Браузер GET /logo.jpg
Сервер Вот файл. Открывай как image/jpeg
🗄️
🖼️ Браузер декодирует и показывает картинку
🌐
Браузер GET /report.zip
Сервер Вот файл. Тип — octet-stream
🗄️
📥 Браузер не знает как открыть — предлагает скачать

Content-Length

Header
Content-Length: 1842

Размер тела ответа в байтах. При стриминге (chunked transfer) этого заголовка нет — размер заранее неизвестен.

Location

Header
Location: /api/users/43
Location: https://new.example.com/page

Set-Cookie

Header
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=3600

Просит браузер сохранить куку.

ПараметрСмысл
HttpOnlyJavaScript не может читать куку — защита от XSS
SecureКука передаётся только по HTTPS
SameSite=StrictКука не отправляется при кросс-сайтовых запросах — защита от CSRF
Max-Age=3600Время жизни в секундах
Domain=.example.comНа каких поддоменах действует
Что происходит когда сервер отправляет Set-Cookie
Ответ сервера после входа
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=3600
Браузер разбирает куку
HttpOnly
JS не может прочитать эту куку — защита от кражи через XSS
Secure
Отправлять только по HTTPS, по HTTP — никогда
SameSite=Strict
Не отправлять при запросах с чужих сайтов — защита от CSRF
Max-Age=3600
Удалить через 3600 секунд (1 час)
Браузер сохраняет и автоматически отправляет с каждым запросом
GET /dashboard HTTP/1.1
Cookie: session_id=abc123 ← добавлено браузером автоматически

Server

Header
Server: nginx/1.24.0

Идентифицирует ПО веб-сервера и его версию.

Cache-Control

Header
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

Header
Content-Encoding: gzip
Content-Encoding: br

Сообщает клиенту каким алгоритмом сжато тело ответа. Работает в паре с Accept-Encoding из запроса — клиент сказал что умеет, сервер выбрал и сообщает что выбрал:

Запрос → Ответ
# Запрос клиента:
Accept-Encoding: gzip, br

# Ответ сервера:
Content-Encoding: gzip
Content-Length: 24817     # размер уже сжатого тела
[тело сжато gzip — браузер распакует автоматически]
ℹ️
Vary: Accept-Encoding Если сервер отдаёт сжатую и несжатую версию одного ресурса, кеш должен хранить их раздельно. Заголовок Vary: Accept-Encoding говорит CDN и прокси: «один URL может иметь разное тело в зависимости от Accept-Encoding — кешируйте варианты отдельно».

Общие заголовки

Connection

Header
Connection: keep-alive
Connection: close

Управляет поведением TCP-соединения. keep-alive — соединение остаётся открытым для следующих запросов. close — закрыть после ответа.

Что такое keep-alive

Без keep-alive каждый HTTP-запрос требует открывать новое TCP-соединение, а если HTTPS — ещё и TLS handshake. Это дорого:

Без keep-alive
TCP handshake → ~150ms
TLS handshake → ~150ms
Запрос/ответ → ~150ms
соединение закрывается

TCP handshake → ~150ms
TLS handshake → ~150ms
Запрос/ответ → ~150ms
и снова...
С keep-alive
TCP handshake → ~150ms ← один раз
TLS handshake → ~150ms ← один раз
Запрос 1 → ~150ms
Запрос 2 → ~150ms ← без handshake
Запрос 3 → ~150ms ← без handshake
соединение остаётся открытым

В HTTP/1.1 keep-alive включён по умолчанию — браузер и сервер переиспользуют соединение автоматически. Но есть нюанс: запросы в одном соединении идут последовательно. Пока не пришёл ответ на первый — второй не отправляется. Это называется head-of-line blocking:

HTTP/1.1 — последовательно
→ GET /index.html
← 200 OK     ← ждём
→ GET /style.css
← 200 OK     ← ждём
→ GET /logo.png
← 200 OK
HTTP/2 — параллельно
→ GET /index.html  ┐
→ GET /style.css   ├ все сразу
→ GET /logo.png   ┘
← 200 OK (style.css)
← 200 OK (logo.png)
← 200 OK (index.html)
💡
HTTP/2 решил проблему head-of-line blocking через мультиплексирование — несколько запросов одновременно в одном соединении. Ответы могут прийти в любом порядке, браузер сам сопоставит их с запросами.

Transfer-Encoding

Header
Transfer-Encoding: chunked

Тело передаётся частями (чанками) — когда общий размер неизвестен заранее. Типично для стриминга, Server-Sent Events, больших файлов. При chunked-кодировании заголовка Content-Length нет.

📦
Обычный ответ
Content-Length известен заранее
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 1842
← размер известен до отправки

{"users": [...]}
Передача
Весь ответ целиком
Клиент ждёт пока сервер сформирует весь ответ
VS
🔄
Chunked Transfer
Размер заранее неизвестен
HTTP/1.1 200 OK
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← конец передачи
Передача
Чанк 1
Чанк 2
Чанк 3
0
Клиент получает и обрабатывает данные по мере поступления
Когда используется chunked
🎬 Видео и аудио стриминг
📡 Server-Sent Events (SSE)
🤖 Ответы AI-моделей (токен за токеном)
📂 Большие файлы на скачивание

Версии HTTP

В лекциях мы используем HTTP/1.1 для примеров — его текстовый формат наглядный и читаемый. Но на практике большинство сайтов уже работают по HTTP/2 или HTTP/3. Разберём три версии чтобы понять чем они отличаются и почему эволюционировали.

HTTP/1.1 (1997)

Текстовый протокол — запрос и ответ можно прочитать глазами. Именно поэтому мы используем его в примерах.

Главное ограничение — head-of-line blocking: в одном TCP-соединении запросы идут строго по очереди. Пока не пришёл ответ на первый — второй ждёт.

ℹ️
Браузеры обходят это открывая 6–8 параллельных TCP-соединений к одному серверу — каждое со своим TCP и TLS handshake. Работает, но дорого.

HTTP/2 (2015)

Бинарный протокол — человек не может прочитать, зато парсится быстрее. Ключевое изменение — мультиплексирование: несколько запросов идут параллельно в одном TCP-соединении, не дожидаясь ответов.

HTTP/1.1 — последовательно
→ GET /index.html
← 200 OK     ← ждём
→ GET /style.css
← 200 OK     ← ждём
→ GET /logo.png
← 200 OK
HTTP/2 — параллельно
→ GET /index.html  ┐
→ 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.1HTTP/2HTTP/3
Формат текстовый бинарный бинарный
Транспорт TCP TCP QUIC (UDP)
Запросы последовательно мультиплекс мультиплекс
Соединений 6–8 параллельно 1 1
TLS опциональный обязательный встроен в QUIC
💡
Заголовки, методы и коды ответа одинаковы во всех версиях HTTP — меняется только то как они упакованы и доставлены.

Заголовки безопасности

Это заголовки ответа которые управляют поведением браузера. Выставляются на уровне веб-сервера и защищают от целого класса атак. Подробно разберём в отдельной теме по безопасности — здесь краткий обзор.

Strict-Transport-Security (HSTS)

Header
Strict-Transport-Security: max-age=31536000; includeSubDomains

Говорит браузеру: «всегда обращайся ко мне только по HTTPS, даже если пользователь напишет http://». Браузер запоминает это на max-age секунд.

Как работает HSTS
Без HSTS — каждый раз уязвимость
👤
Пользователь вводит http://bank.com
⚠️
Браузер делает запрос по HTTP
Трафик не зашифрован — можно перехватить
🗄️
Сервер редиректит на HTTPS 301
Но момент первого HTTP-запроса уже был уязвим
С HSTS — браузер не допускает HTTP
🗄️
Сервер однажды ответил с заголовком:
Strict-Transport-Security: max-age=31536000
🌐
Браузер запомнил на 31 536 000 сек
≈ 1 год
👤
Пользователь вводит http://bank.com
🌐
Браузер сам меняет на https://bank.com
HTTP-запрос до сервера не доходит вообще

X-Frame-Options

Header
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN

Запрещает встраивать страницу в <iframe> на чужих сайтах. Защита от clickjacking — когда злоумышленник накладывает прозрачный iframe поверх своей страницы и перехватывает клики пользователя.

X-Content-Type-Options

Header
X-Content-Type-Options: nosniff

Запрещает браузеру угадывать тип контента. Без этого заголовка браузер может интерпретировать картинку как HTML с JavaScript внутри.

Content-Security-Policy (CSP)

Header
Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com

Самый мощный заголовок безопасности. Говорит браузеру откуда разрешено загружать ресурсы — скрипты, стили, картинки, фреймы. Основная защита от XSS-атак.