Справочник по HTTP-заголовкам: как правильно выбирать Content-Type, Cache-Control и Authorization
Сконцентрируемся на заголовках, которые чаще всего ломают интеграции и кэширование: что они означают и какие комбинации реально безопасны.
Содержание
Справочник по HTTP-заголовкам: как правильно выбирать Content-Type, Cache-Control и Authorization
Интеграции ломаются не от «сложности HTTP», а от мелочей: один сервис неверно выбирает Content-Type, другой не учитывает особенности кэширования, третий по-разному трактует Authorization. В результате вы получаете редкие, трудно воспроизводимые баги: «иногда работает», «кэш залипает», «у некоторых клиентов 401, у других — 200», «а что именно в заголовках — никто не помнит».
В этом материале — практический справочник по трём категориям заголовков, которые чаще всего становятся причиной проблем: Content-Type, Cache-Control и Authorization. Разберём смысл, безопасные комбинации, типичные ошибки и реальные сценарии (в том числе для интеграций и публичных API).
Почему именно эти заголовки чаще всего ломают всё
HTTP-заголовки — это контракт между клиентом, сервером и промежуточными узлами (прокси, балансеры, CDN). Ошибка в заголовке — это ошибка в трактовке контракта:
Content-Typeвлияет на то, как клиент и промежуточные компоненты интерпретируют тело запроса/ответа. Неправильный тип = неверное декодирование, ошибки сериализации, некорректная обработка на уровне фреймворков.Cache-Controlопределяет, можно ли хранить ответ и при каких условиях. Ошибка в правилах кэширования = утечки данных или «вечный» кэш неверной версии.Authorizationуправляет доступом и критичен к формату. Малейшее отклонение от ожидаемой схемы/формата = 401/403 и «плавающие» проблемы в разных клиентах.
Content-Type: как выбирать корректно и не ломать клиентов
Что такое Content-Type на практике
Content-Type в HTTP задаёт медиа-тип содержимого тела. Это базовая информация для парсинга:
application/json— тело JSON.application/xml— XML.text/plain; charset=utf-8— текст.multipart/form-data; boundary=...— смешанные части формы.application/x-www-form-urlencoded— типичное представление формы как ключ=значение&...
На сервере Content-Type влияет на то, как фреймворк решит читать тело. На клиенте — как он решит сериализовать/декодировать.
Важно:
Content-Typeописывает тело в конкретном сообщении, а не «вообще формат API».
Content-Type для запросов (request): что бывает неправильно
Типовые ошибки:
- Неправильный медиа-тип при отправке JSON
Отправляете JSON-строку, но ставитеContent-Type: text/plain. Некоторые клиенты/библиотеки начнут распознавать неверно (или не распознают вообще). - Отсутствие
charsetтам, где это критично для текста
Дляtext/*и некоторых других типов полезно явно задаватьcharset=utf-8. По умолчанию многие системы пытаются угадать, а «угадывание» в разных средах может отличаться. - Путаница между
application/jsonиapplication/problem+json
Для ошибок часто используют RFC 7807. Это всё ещё JSON, но семантика другая. Если клиент ожидает один тип, а вы отдаёте другой — он может упасть при проверках. - Неправильная кодировка base64
Например, отправляете бинарные данные в JSON, но не описываете формат и не согласуете. Тут чаще проблема не вContent-Type, а в том, что вы по факту не описали payload.
Content-Type для ответов (response): согласованность — ключ
Если ваша ручка возвращает JSON — отдавайте:
Content-Type: application/json; charset=utf-8(частоcharsetне обязателен, но для совместимости полезен)
Если возвращаете PDF:
Content-Type: application/pdf
Если возвращаете файл:
Content-Type: application/octet-stream- дополнительно разумно указать
Content-Disposition: attachment; filename="..."
Если отдаёте HTML:
Content-Type: text/html; charset=utf-8
Пример: корректный JSON-ответ и коды
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
{"id": 123, "status": "active"}
Для валидационных ошибок (опционально, но корректно и предсказуемо):
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
{
"type": "https://example.com/probs/invalid-input",
"title": "Invalid input",
"status": 400,
"detail": "Field 'email' is invalid"
}
Какие комбинации безопасны
Для типичных API «почти всегда безопасные» правила такие:
- Если тело — JSON →
Content-Type: application/json(илиapplication/problem+jsonдля ошибок по RFC 7807). - Если тело — текст →
text/plain; charset=utf-8или более конкретныйtext/*. - Если тело — бинарные данные → один из:
application/octet-stream- или конкретный тип (например
image/png,application/pdf)
- Если это multipart →
multipart/form-data; boundary=...— граница должна соответствовать реальному формату тела.
Подводные камни интеграций: «мы же отправляли JSON!»
В интеграциях часто встречается ситуация: сервер ожидает application/json, а клиент отправляет application/json; charset=ISO-8859-1 или вообще text/json. В большинстве случаев парсинг JSON будет работать, но:
- Некоторые middleware (или шлюзы безопасности/WAF) могут применять правила по точному совпадению.
- Логирование/обнаружение контента может делать неправильные выводы.
- В редких случаях клиентская библиотека может выбрать неверный декодер и получить «битый» JSON.
Рекомендация: держите строгий контракт. Если вы принимаете JSON — принимайте разумный набор вариантов, но нормализуйте на своей стороне. Например, на сервере можно трактовать application/*+json как JSON (это полезно для application/vnd.api+json, application/problem+json и т.п.), но не стоит бесконечно расширять список.
Cache-Control: кэширование без сюрпризов
Базовая логика HTTP-кэширования
Кэширование в HTTP — это компромисс между производительностью и корректностью. Cache-Control задаёт правила, но есть нюансы:
- У разных посредников могут быть свои ограничения и политики.
- Клиенты и CDN соблюдают заголовки по-разному, особенно если есть требования безопасности.
- Есть ещё валидаторы (
ETag,Last-Modified), которые часто работают совместно с кэшем.
Тем не менее, Cache-Control — главный инструмент.
Разберём часто используемые директивы
Ниже — практический набор, который стоит знать.
no-store
Жёсткая директива: не сохранять ответ нигде, не вести запись в кэши.
- Используйте для ответов, содержащих чувствительные данные (персональные токены, приватные данные, «секреты»).
- Подходит для интеграций, где важна корректность и нельзя допустить сохранение.
Пример:
Cache-Control: no-store
no-cache
Технически разрешает хранить, но требует проверку у источника перед использованием.
Часто путают с no-store. Разница концептуальная:
no-store— «не сохранять»no-cache— «можно хранить, но каждый раз валидировать»
На практике, если у вас нет корректных валидаторов (ETag/Last-Modified) — no-cache может дать странный эффект: кэш может всё равно пытаться использовать/проверять, а проверка будет невозможна.
Пример:
Cache-Control: no-cache
max-age=<seconds>
Сколько секунд ответ считается свежим.
- Хорошо для публичных и неизменяемых ресурсных данных (справочники, статические конфиги версий, метаданные).
- Для динамики с разной персонализацией — осторожно.
Пример:
Cache-Control: public, max-age=300
public / private
public— кэшировать могут и промежуточные узлы (CDN, прокси).private— кэширование разрешено только в контексте клиента (обычно в браузере/клиентском кэше), но не CDN.
Типичная ошибка: поставить public для ответов, зависящих от пользователя. Это открывает риск утечки при неправильном кэшировании.
Пример:
Cache-Control: private, max-age=60
must-revalidate
Если кэш стал «просроченным», нужно обязательно перепроверить. Полезно, когда вы позволяете хранить данные, но не хотите долгих «устаревших» окон.
Пример:
Cache-Control: private, max-age=60, must-revalidate
s-maxage=<seconds>
Как max-age, но применяется к shared caches (CDN/прокси), а не к персональному кэшу.
Пример:
Cache-Control: public, s-maxage=300
Это помогает, когда вы хотите кэшировать на CDN, но более строго контролировать браузер.
immutable
Если ответ с этим заголовком соответствует неизменяемому ресурсу (например, ассет с хэшом в имени файла), клиент может не перепроверять его.
Пример:
Cache-Control: public, max-age=31536000, immutable
Практические безопасные пресеты для API
Ниже набор «почти всегда безопасных» стратегий.
Приватные/персональные ответы (почти всегда)
Cache-Control: no-store
Если нужно оставить минимальную возможность кэширования на клиенте — иногда используют private, no-store или private, max-age=... для не-очень-секретных данных, но это требует аккуратной модели угроз и тестирования.
Для интеграций с токенами и персональными данными самый безопасный стандарт: no-store.
Публичные справочные данные (можно кэшировать)
Cache-Control: public, max-age=<small number>- или
public, s-maxage=<seconds>если хотите больше контроля на CDN
Но важно: данные должны быть либо неизменяемыми, либо с корректным сроком жизни и механизмом инвалидции.
Отладка/частые изменения: нейтральный вариант
Cache-Control: no-cache
Это «компромисс», но опять же зависит от того, есть ли у вас внятная схема валидаторов (обычно ETag).
Частая ошибка: кэширование 401/403/500
Большая часть сервисов допускает кэширование ошибок по ошибке, особенно через промежуточные компоненты. Это опасно и приводит к «залипанию» состояния.
Рекомендация: для ошибок, связанных с аутентификацией/авторизацией или сессиями, используйте:
Cache-Control: no-store
Пример:
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
{"error":"unauthorized"}
Cache-Control и ETag: совместимость, которую нельзя игнорировать
Cache-Control управляет свежестью, а ETag позволяет корректно валидировать кэш. Типовой безопасный сценарий:
- Ответ содержит
ETag - Кэш хранит ответ
- Клиент/прокси делает запрос с
If-None-Match - Сервер возвращает
304 Not Modifiedпри совпадении
Пример:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=60
ETag: "v3-abc123"
{"version":3,"data":"..."}
Если валидаторы отсутствуют, а вы используете no-cache, поведение может быть менее предсказуемым.
Authorization: формат схемы и защита от несовместимостей
Что означает Authorization
Authorization в заголовках запроса обычно содержит креденшелы в одной из схем:
Bearer <token>(самый распространённый вариант в OAuth 2.0 / OpenID Connect)Basic <base64(username:password)>- Реже:
Digest,HOBA, кастомные схемы.
Критически важно: клиент и сервер должны договориться о схеме. Authorization сам по себе не говорит, какую схему вы выбрали — это часть строки.
Самые частые ошибки формата
- Отсутствие пробела после схемы
Должно бытьBearer <token>, а неBearer<token>. - Смешение регистров/неожиданная капитализация
Схемы обычно нечувствительны к регистру, но строгие парсеры иногда капризны. - Неверная строка для Basic
Basicтребуетbase64(username:password)без переводов строки и корректной кодировки. - Отправка уже «разобранного» токена
Например, клиент формирует заголовок какAuthorization: Bearerи отдельным параметром кладёт токен в query. Это часто ломает сервер и провоцирует утечки (query может логироваться и кэшироваться).
Безопасные правила работы с Authorization в интеграциях
- Всегда отправляйте токен только в заголовке, а не в URL.
- Для публичного кэширования (CDN) учитывайте, что авторизованные ответы не должны кэшироваться без явной модели. На уровне API часто проще использовать:
Cache-Control: no-storeдля ответов, зависящих от токена- корректную проверку
Vary(см. ниже)
Пример запроса с Bearer
GET /v1/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Accept: application/json
Нужно ли использовать Vary для Authorization?
Сам по себе Vary относится к кэшированию: он говорит кэшу, по каким заголовкам различать ответы. Если вы отдаёте разные ответы в зависимости от Authorization, и при этом допускаете кэширование — нужно обеспечить, чтобы кэш корректно различал запросы.
Практическая стратегия для большинства интеграций:
- Не допускать кэширование (
no-store) для авторизованных ответов — самый надёжный путь. - Если вы вынуждены кэшировать (например, публичные данные, но доступны через токен) — используйте
Vary: Authorization(и/или другие заголовки), но тестируйте на конкретной инфраструктуре (CDN/прокси) — разные узлы ведут себя по-разному.
Пример:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: private, max-age=60
Vary: Authorization
Комбинации: как сочетать Content-Type, Cache-Control и Authorization безопасно
Теперь перейдём к главному — к тому, что реально ломает интеграции: «комбо-заголовки».
Сценарий 1: Авторизованный JSON-ответ без кэширования
Когда ответ зависит от пользователя/токена:
Content-Type: application/json; charset=utf-8Cache-Control: no-storeAuthorization— в запросе, а не в ответе (обычно)
Пример:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
{"userId": 42, "roles":["admin"]}
Это правило снижает риск «подмешивания» чужого кэша.
Сценарий 2: Публичный JSON-эндпоинт, который можно кэшировать на CDN
Content-Type: application/json; charset=utf-8Cache-Control: public, s-maxage=300(илиmax-age)Authorizationотсутствует (или используется только для расширенных данных — тогда осторожно)
Пример:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, s-maxage=300, must-revalidate
ETag: "catalog-9f2d"
Сценарий 3: Ошибка авторизации
Здесь важно не кэшировать, потому что причина может измениться (токен истёк/обновился):
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
{"error":"invalid_token"}
Сценарий 4: Файлы и вложения — где часто путают типы
Если вы отдаёте бинарный файл, Content-Type должен быть корректным, иначе клиент может открыть файл как текст или испортить скачивание.
Пример:
HTTP/1.1 200 OK
Content-Type: application/pdf
Cache-Control: private, max-age=600
...binary...
Полевые заметки: как отлаживать проблемы с заголовками
1) Всегда сверяйте «что реально ушло по wire»
Многие баги не в логике, а в расхождении между тем, что вы думаете, и тем, что реально отправлено:
Комментарии
Пока нет комментариев