На тестовом домене карточка по адресу /catalog должна отвечать на русском и английском в зависимости от Accept-Language. После включения общего кэша часть английских запросов получает русский HTML, хотя origin формирует правильный язык. Цена ошибки — не косметика: пользователь видит чужой интерфейс, а команда может ошибочно списать проблему на перевод или браузер вместо того, чтобы проверить ключ сохранённого ответа.
Ниже полевой сценарий для одной ветки: один URL, два языка, доступ к безопасному тестовому origin и к публичному адресу через CDN. Он не предполагает, что любой CDN автоматически уважает каждый Vary. Сначала докажем, что origin различает варианты и объявляет это в HTTP, затем проверим тот же договор на публичном пути. Никаких личных cookies и настоящих пользовательских страниц в команду не подставляем.
Определяем, что считается разным представлением
Один URL может иметь несколько представлений. В нашем примере тело меняется из-за Accept-Language: меняются заголовок, подписи и ссылка на локализованный asset. Это не вопрос вкуса, а вход генерации ответа. RFC описывает Vary как список полей запроса, которые повлияли на выбранное представление. Поэтому origin обязан не только вернуть русский текст, но и объявить кэшу, что язык участвовал в выборе.
Не добавляйте Vary: Cookie по инерции. Если cookie содержит идентификатор сессии, такой ключ может раздробить общий кэш на множество значений и всё равно не решить персональные данные. Сначала ответьте, почему тело меняется. Для личной страницы правильный маршрут часто начинается с private, а для публичной локализации — с ограниченного и проверяемого Vary: Accept-Language.
| Сценарий | Ожидаемый заголовок | Что считаем ошибкой | Следующее действие |
|---|---|---|---|
| Публичный русский и английский HTML | Vary: Accept-Language | Тела различаются, а Vary отсутствует или не совпадает | Исправить origin и повторить два запроса |
| Одинаковый ответ независимо от языка | Vary не требуется только ради предположения | Добавлен лишний Vary без отличий тела | Убрать лишний вариант и измерить ключ заново |
| Страница с пользователем | private или более строгий запрет хранения | Общий кэш способен использовать ответ другой сессии | Отделить публичный shell от личных данных |
| Вариант устарел после срока | ETag и условный запрос при выбранном договоре | Сервер всегда отдаёт тело, хотя представление не менялось | Проверить генерацию валидатора и путь до origin |
Собираем контрольный запрос к origin
Начинаем не с интерфейса, а с заголовков. Два запроса должны отличаться ровно одним входом — языком. Сохраняем заголовки и тело раздельно, чтобы не перепутать вывод curl с данными. В боевом окружении тестовый origin должен быть доступен безопасным способом: отдельный host, allowlist или стенд. Не обходите аутентификацию и не добавляйте служебные адреса в публичные примеры.
# Русский вариант.
curl -sS -D /tmp/catalog-ru.headers -o /tmp/catalog-ru.html -H "Accept-Language: ru" https://origin.example.test/catalog
# Английский вариант: меняется только один вход.
curl -sS -D /tmp/catalog-en.headers -o /tmp/catalog-en.html -H "Accept-Language: en" https://origin.example.test/catalog
grep -Ei "^(cache-control|vary|etag|last-modified):" /tmp/catalog-ru.headers
grep -Ei "^(cache-control|vary|etag|last-modified):" /tmp/catalog-en.headers
Ожидаем не конкретный текст ETag, а форму договора. В обоих ответах должно быть одинаковое правило кэширования, если срок свежести одинаков. Vary должен перечислять Accept-Language, если тела различаются по этому полю. ETag может различаться, потому что представления различаются. Если origin уже возвращает неверные заголовки, CDN пока не трогаем: сначала исправляем источник, иначе edge-диагностика будет смешивать две ошибки.
Повторяем сценарий через публичный путь
Когда origin прошёл проверку, повторяем те же два запроса через публичный адрес. Менять одновременно URL, язык, User-Agent и cookie нельзя: тогда результат невозможно объяснить. Сначала сравниваем headers с origin: они могут дополняться, но Cache-Control, Vary и валидатор не должны потерять смысл. Затем сравниваем тела или их безопасные хеши. Если публичный путь отдаёт один вариант на два языка, фиксируем это как расхождение cache key, а не как «кэш иногда глючит».
for lang in ru en; do
curl -sS -D "/tmp/edge-$lang.headers" -o "/tmp/edge-$lang.html" -H "Accept-Language: $lang" https://www.example.test/catalog
done
sha256sum /tmp/edge-ru.html /tmp/edge-en.html
grep -Ei "^(cache-control|vary|etag|age):" /tmp/edge-ru.headers
grep -Ei "^(cache-control|vary|etag|age):" /tmp/edge-en.headers
Фрагмент предназначен для shell с безопасным доменом; он не является выполненным измерением этой статьи. Важна последовательность: сначала подтверждаем два варианта, потом смотрим, что edge не склеил их в один. Поле Age, если его отдаёт слой, помогает понять, что ответ уже жил в кэше, но его отсутствие не доказывает отсутствие кэширования. Конкретные диагностические заголовки CDN не универсальны и должны быть описаны его документацией.
Проверяем повторную проверку после истечения свежести
Второй тип сбоя выглядит иначе: варианты различаются правильно, но после изменения перевода один из них долго остаётся старым. Здесь проверяем валидатор. Сохраняем ETag русского варианта, ждём или на тестовом стенде настраиваем короткий срок свежести, затем посылаем If-None-Match для того же языка. Если тело не менялось, допустим 304; если менялось — ожидаем новый 200 и новый ETag. Нельзя проверять русский валидатор английским запросом: это уже другой вариант.
# Значение взять из ответа русского варианта, не подставлять личные токены.
curl -sS -D - -o /dev/null -H "Accept-Language: ru" -H 'If-None-Match: "catalog-ru-v18"' https://www.example.test/catalog
Если ответ всегда 200, это не повод отключить кэш. Сначала выясняем, меняется ли ETag на каждом запросе из-за времени, случайного идентификатора или неустойчивого порядка данных. Валидатор должен описывать представление, а не шум вокруг него. Если сервер всегда 304 после реального изменения текста, наоборот, валидатор слишком грубый. Оба случая проверяются на маленьком контролируемом изменении, а не на общей очистке всей зоны.
Матрица решения по наблюдению
Результат сценария удобно зафиксировать как четыре короткие строки: вариант запроса, URL, digest тела и заголовки. В локализации это даёт картину, которую можно показать владельцу CDN или backend: вот два запроса, вот origin, вот edge, вот поле, исчезнувшее на переходе. Такой артефакт ценнее скриншота, потому что его можно повторить после правки правила.
| Наблюдение после двух запросов | Граница, где искать | Безопасная следующая проверка |
|---|---|---|
| Origin возвращает один и тот же язык | Шаблон/роутинг origin | Проверить, доходит ли Accept-Language до приложения |
| Origin различает, но не ставит Vary | HTTP-ответ приложения или proxy | Добавить Vary для реального входа и снова снять заголовки |
| Origin корректен, edge склеивает варианты | Настройка CDN/cache key | Сверить правило провайдера с входом языка на тестовом URL |
| Варианты разделены, но новый текст не приходит после срока | ETag, TTL или маршрут условного запроса | Проверить один вариант с If-None-Match |
Порядок внедрения
- Выберите публичный тестовый URL без личных данных и один вход, который точно меняет тело.
- Снимите два ответа origin, сохранив тело и ключевые заголовки по отдельности.
- Проверьте соответствие между различием тела и
Vary; не расширяйте key произвольными полями. - Повторите сценарий через CDN с теми же двумя запросами и зафиксируйте расхождение.
- Проверьте один язык условным запросом после выбранного срока свежести.
- После правки оставьте команды и ожидаемые признаки в репозитории или runbook, чтобы следующий релиз не начинал диагностику заново.
Ограничения и следующий шаг
Сценарий не проверяет все возможные варианты: мобильный рендер, эксперимент, гео и авторизацию нужно добавлять отдельно только если они действительно меняют тело. Он также не разрешает кешировать персональный HTML. Его задача уже: показать, что правильный перевод на origin не равен правильному cache key на edge.
После этой проверки можно переходить к настройке TTL и purge-процесса конкретного провайдера, но только с зафиксированным ключом. Если команда не может назвать, какие поля запроса создают разные представления, сначала вернитесь к шаблону и данным. Для автора 2019 года это развитие от ручного curl-диагноза к границе между HTTP-договором и инфраструктурой доставки, без притворной уверенности, что один заголовок управляет всей сетью.
Короткий вывод
- Два языка по одному URL требуют проверяемого различия вариантов, а не только двух правильных HTML на origin.
Varyотражает входы, которые изменили представление; он не заменяет проверку реальной настройки CDN.- ETag проверяется внутри одного варианта запроса, иначе 304 и 200 нельзя интерпретировать.
- Небольшой повторяемый сценарий с headers и телом быстрее локализует сбой, чем очистка всего кэша.
Проверяемые источники
- RFC 7234, раздел 5.2: Cache-Control — семантика max-age, s-maxage, private, no-cache и no-store для HTTP/1.1-кэшей
- RFC 7234, раздел 4.3: validation — условные запросы, ETag, If-None-Match и ответ 304 как повторная проверка сохранённого представления
- RFC 7232, раздел 2.3: ETag — синтаксис entity-tag и связь валидатора с конкретным представлением ресурса
- RFC 7234, раздел 4.1: Vary — как кэш выбирает подходящую сохранённую вариацию ответа по полям запроса
- MDN: HTTP caching — практическое объяснение свежести, повторной проверки, ETag и разницы между no-cache и no-store
- MDN: Vary header — Vary как перечисление полей запроса, которые повлияли на представление ресурса