Files
progcode/editorial/agent-rewrites/310.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
16 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 310,
"slug": "editorial-2019-05-field-http-caching",
"title": "Один URL, два ответа: как не склеить варианты в HTTP-кэше",
"excerpt": "После включения CDN пользователь получает не тот язык или старую версию страницы. Разбираем cache key, Vary и ETag на контролируемом примере и проверяем результат через origin и edge.",
"contentHtml": "<p>После деплоя <code>/catalog</code> должен отдавать английский HTML по запросу с <code>Accept-Language: en</code>. Но часть пользователей видит русский текст. Иногда проблема выглядит мягче: новая подпись уже есть на origin, а публичный адрес ещё отдаёт старую. Цена ошибки — неверный интерфейс, потерянное доверие и часы на спор между frontend, backend и CDN. Если в ответе есть персональные данные, ошибка кэша становится утечкой.</p>\n<p>Тезис простой: кэш хранит не «URL вообще», а представление ответа для конкретного запроса. Чтобы использовать сохранённый ответ безопасно, нужно согласовать четыре вещи: какие входы меняют тело, как они попадают в cache key, сколько ответ считается свежим и как сервер подтверждает его версию. Один правильный заголовок не исправляет расхождение между приложением и edge.</p>\n<h2>Механизм: представление, вариант и свежесть</h2>\n<p>В учебном примере тело <code>/catalog</code> зависит только от <code>Accept-Language</code>. Русский и английский ответы имеют один URL, но являются разными представлениями. Origin должен сообщить это через <code>Vary: Accept-Language</code>. Кэш использует указанные поля запроса, чтобы отличить сохранённые варианты. Если поле не указано, edge может считать русский и английский ответы одним объектом.</p>\n<p><code>Cache-Control</code> задаёт правила хранения и повторного использования. <code>max-age</code> ограничивает свежесть ответа для обычного кэша. <code>s-maxage</code> позволяет отдельно задать срок для shared cache, если это нужно архитектуре. <code>private</code> запрещает общий кэш для ответа, который нельзя отдавать другой сессии. <code>no-store</code> запрещает хранение, но не превращает любой публичный ответ в персональный.</p>\n<p>После истечения свежести кэш может не получать всё тело заново. Он отправляет условный запрос с валидатором. Для этого сервер возвращает <code>ETag</code>, а клиент или кэш повторяет его в <code>If-None-Match</code>. Если представление не изменилось, сервер отвечает <code>304 Not Modified</code>. Если изменилось, он возвращает новое тело и новый валидатор. Проверяйте ETag внутри одного варианта: русский ETag нельзя интерпретировать запросом без того же языка.</p>\n<pre><code>GET /catalog HTTP/1.1\nHost: www.example.test\nAccept-Language: en\n\nHTTP/1.1 200 OK\nCache-Control: public, max-age=60\nVary: Accept-Language\nETag: \"catalog-en-v18\"\n\n&lt;h1&gt;Catalog&lt;/h1&gt;</code></pre>\n<p>Это учебный обмен. Значение <code>ETag</code> придумано для примера и не является результатом измерения. В реальном сервисе валидатор должен меняться вместе с выбранным представлением, а не с каждым запросом из-за случайного идентификатора.</p>\n<h2>Сначала докажите различие на origin</h2>\n<p>Начните с безопасного тестового домена. Снимите два ответа, изменив только язык. Сохраните заголовки и тела отдельно. Не добавляйте cookies, User-Agent и экспериментальные параметры одновременно: иначе нельзя понять, какой вход изменил результат.</p>\n<pre><code>curl -sS -D /tmp/catalog-ru.headers \\\n -o /tmp/catalog-ru.html \\\n -H \"Accept-Language: ru\" \\\n https://origin.example.test/catalog\n\ncurl -sS -D /tmp/catalog-en.headers \\\n -o /tmp/catalog-en.html \\\n -H \"Accept-Language: en\" \\\n https://origin.example.test/catalog\n\nsha256sum /tmp/catalog-ru.html /tmp/catalog-en.html\ngrep -Ei \"^(cache-control|vary|etag|last-modified):\" /tmp/catalog-ru.headers\ngrep -Ei \"^(cache-control|vary|etag|last-modified):\" /tmp/catalog-en.headers</code></pre>\n<p>Команды предназначены для стенда и не выполнены в рамках этой статьи. На origin ожидайте разные тела, если язык действительно влияет на HTML. При этом <code>Vary</code> должен назвать <code>Accept-Language</code>. Если тела одинаковые, не добавляйте <code>Vary</code> по догадке: лишний вариант уменьшит эффективность кэша. Если origin уже отдаёт неверный язык, не начинайте с purge CDN. Исправьте выбор представления или передачу заголовка.</p>\n<figure><img src=\"/assets/editorial/2019/http-cache-variant-check-2019.svg\" alt=\"Проверка русской и английской версии через origin, CDN и условный запрос с ETag\" /><figcaption>Один URL может иметь несколько представлений. Язык должен участвовать в проверяемом договоре кэширования.</figcaption></figure>\n<h2>Потом сравните публичный путь</h2>\n<p>Повторите те же запросы через CDN. Сравните статус, тело и ключевые заголовки. Edge может добавлять служебные поля, например <code>Age</code>, но не должен терять смысл <code>Vary</code>, срока свежести и валидатора. Отсутствие <code>Age</code> само по себе не доказывает, что origin был вызван.</p>\n<pre><code>for lang in ru en; do\n curl -sS -D \"/tmp/edge-$lang.headers\" \\\n -o \"/tmp/edge-$lang.html\" \\\n -H \"Accept-Language: $lang\" \\\n https://www.example.test/catalog\ndone\n\nsha256sum /tmp/edge-ru.html /tmp/edge-en.html\ngrep -Ei \"^(cache-control|vary|etag|age):\" /tmp/edge-ru.headers\ngrep -Ei \"^(cache-control|vary|etag|age):\" /tmp/edge-en.headers</code></pre>\n<p>Если origin различает ответы, а edge отдаёт одинаковое тело, ищите расхождение в настройке cache key или в том, как провайдер обрабатывает <code>Vary</code>. Не каждый CDN одинаково строит ключ по заголовкам. Документация провайдера важна, но её нужно подтвердить двумя реальными запросами на тестовом URL.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Origin отдаёт один язык</td><td>Приложение не использует вход или прокси его теряет</td><td>Два запроса к origin, тела и входящие заголовки</td><td>Исправить маршрутизацию или генерацию; CDN не менять</td></tr><tr><td>Origin различает, но <code>Vary</code> отсутствует</td><td>HTTP-договор не описывает зависимость</td><td>Сравнить тела и response headers</td><td>Добавить реальный вход в <code>Vary</code>; повторить тест</td></tr><tr><td>Origin корректен, edge склеивает варианты</td><td>Cache key не учитывает язык или правило провайдера не применилось</td><td>Одинаковые запросы через публичный URL, digest тел</td><td>Исправить правило CDN и проверить два холодных варианта</td></tr><tr><td>Новая версия приходит только после purge</td><td>Слишком длинный TTL или неработает revalidation</td><td>Тот же язык, ETag и <code>If-None-Match</code> после выбранного срока</td><td>Настроить TTL и валидатор; purge оставить аварийным инструментом</td></tr><tr><td>Личная страница попадает в общий кэш</td><td>Ответ помечен как публичный или ключ неполон</td><td>Проверить cookies, Authorization и <code>Cache-Control</code></td><td>Разделить публичный shell и данные либо использовать <code>private</code>/<code>no-store</code></td></tr></tbody></table></div>\n<h2>Проверяем ETag без ложного вывода</h2>\n<p>Выберите один язык и короткий срок на тестовом стенде. Сначала получите ETag. Затем отправьте условный запрос с тем же <code>Accept-Language</code> и этим ETag.</p>\n<pre><code>curl -sS -D - -o /dev/null \\\n -H \"Accept-Language: ru\" \\\n -H 'If-None-Match: \"catalog-ru-v18\"' \\\n https://www.example.test/catalog</code></pre>\n<p>Если представление не менялось и валидатор совпадает, ожидайте <code>304</code>. После контролируемого изменения текста ожидайте <code>200</code> с новым телом и новым ETag. Если сервер всегда возвращает <code>200</code>, проверьте, не меняется ли ETag из-за времени или случайного поля. Если сервер возвращает <code>304</code> после изменения, валидатор слишком грубый. Это учебные критерии; конкретные статусы нужно подтвердить на вашем стенде.</p>\n<h2>Порядок действий</h2>\n<ol><li>Выберите публичный тестовый URL без персональных данных и назовите единственный вход, который должен менять тело.</li><li>Снимите два ответа origin: сохраните заголовки, тела и значения входов отдельно.</li><li>Сравните тела. Если они различаются, проверьте <code>Vary</code> и не расширяйте ключ случайными полями.</li><li>Повторите те же два запроса через CDN. Зафиксируйте digest тел, <code>Cache-Control</code>, <code>Vary</code>, <code>ETag</code> и доступные служебные поля.</li><li>Для одного варианта выполните условный запрос с <code>If-None-Match</code> после выбранного срока свежести.</li><li>Исправьте слой, на котором появилось расхождение: приложение, proxy или cache key. Не начинайте с полной очистки кэша.</li><li>Повторите положительный и отрицательный сценарии: правильный язык и попытку получить другой язык из того же URL.</li><li>Оставьте команды и ожидаемые признаки в runbook или автоматической проверке.</li></ol>\n<h2>Ограничения</h2>\n<p>Один заголовок не описывает всю конфигурацию CDN. Провайдер может иметь собственные правила нормализации, дополнительные части ключа и отдельные механизмы purge. Их нужно проверять по документации и на вашем тестовом URL.</p>\n<p>Если тело зависит от cookie, Authorization, географии, устройства или эксперимента, добавьте каждый реальный вариант в модель. Не превращайте <code>Vary: Cookie</code> в универсальный ремонт: он может раздробить кэш и не решить задачу персональных данных. Для личного HTML безопаснее отказаться от shared cache.</p>\n<p>Сценарий не доказывает production-результаты и не заменяет нагрузочные тесты. Учебные домены, ETag и digest выше нужны только для воспроизводимой проверки механизма. Переход к production требует согласованного TTL, политики очистки, наблюдаемости и владельца каждого правила.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Работа готова, когда два запроса к origin дают ожидаемые представления, response headers объявляют реальные входы через <code>Vary</code>, а два запроса через CDN возвращают соответствующие тела, а не первый сохранённый вариант. Для одного языка условный запрос возвращает <code>304</code> без изменения представления и <code>200</code> с новым ETag после контролируемого изменения. Персональный ответ не доступен через общий кэш. Все четыре результата записаны в повторяемую проверку.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html\" target=\"_blank\" rel=\"noopener\">RFC 9111: HTTP Caching</a> — правила свежести, хранения и расчёта вариантов через <code>Vary</code>.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener\">RFC 9110: HTTP Semantics</a> — семантика представлений, <code>ETag</code>, условных запросов и <code>Vary</code>.</li></ul>"
}