8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"index": 312,
|
||
"slug": "editorial-2019-05-practice-http-caching",
|
||
"title": "HTTP-кэширование без устаревших ответов: контракт для HTML, assets и API",
|
||
"excerpt": "После релиза браузер показывает старый HTML, JavaScript или данные API. Разбираем, как связать cache key, свежесть, валидаторы и проверку ответа, чтобы кэш ускорял сайт, а не скрывал ошибку.",
|
||
"contentHtml": "<p>После релиза пользователь открывает карточку товара и видит вчерашнюю цену. Страница загрузилась быстро. В DevTools почти нет ошибок. Но HTML ссылается на старый JavaScript, а CDN отдаёт прежний JSON. Команда меняет <code>max-age</code> и не понимает, какой слой сохранил ответ.</p>\n<p>Цена ошибки выше, чем лишний запрос. Пользователь принимает решение по неверным данным. Новый код может работать со старой разметкой. Если ответ персональный, общий кэш может показать его другому пользователю. Простое «очистим кэш» убирает симптом, но не объясняет причину.</p>\n<p>Тезис статьи простой: HTTP-кэширование — это контракт ресурса, а не одно число в заголовке. Контракт описывает ключ ответа, допустимую давность, правила повторной проверки и границы хранения. Пока эти четыре пункта не проверены, <code>Cache-Control</code> не доказывает, что приложение отдаёт нужную версию.</p>\n<h2>Сначала определите, что именно кэшируется</h2>\n<p>Кэш хранит представление ответа. Минимальный ключ включает метод запроса и целевой URI. На представление могут влиять заголовки запроса, например <code>Accept-Language</code>, параметры, cookie или авторизация. Если русский и английский HTML приходят по одному URL, кэш должен различать варианты. Для заголовков согласования ответа используют <code>Vary</code>; для cookie и авторизации обычно требуется отдельная политика хранения или явный cache key на промежуточном слое.</p>\n<p>Стабильный URL и изменяемое содержимое требуют осторожного договора. HTML по адресу <code>/catalog</code> обычно должен быстро перепроверяться, потому что он содержит ссылки на текущие assets. Файл <code>/assets/app.4f91.js</code> может храниться долго: при изменении содержимого сборка создаёт новый URL. Ответ <code>/api/catalog?category=12</code> можно кэшировать на короткий срок только после проверки его входов и допустимой давности.</p>\n<table><thead><tr><th scope=\"col\">Ресурс</th><th scope=\"col\">Условие</th><th scope=\"col\">Пример договора</th><th scope=\"col\">Риск</th></tr></thead><tbody><tr><td>Общий HTML</td><td>Тело не зависит от пользователя</td><td><code>no-cache</code> и валидатор</td><td>Старая ссылка на asset</td></tr><tr><td>Файл с хешем в имени</td><td>Новый байтовый состав получает новый URL</td><td><code>public, max-age=31536000, immutable</code></td><td>Старый код живёт по постоянному адресу</td></tr><tr><td>Общий API-ответ</td><td>Ключ учитывает все варианты</td><td>Короткий <code>max-age</code> или <code>s-maxage</code></td><td>Устаревшие или смешанные данные</td></tr><tr><td>Персональный ответ</td><td>Тело зависит от сессии</td><td><code>private</code> или <code>no-store</code></td><td>Утечка через shared cache</td></tr></tbody></table>\n<p>Это не универсальная таблица заголовков. Она задаёт порядок решения. Сначала найдите входы, которые меняют тело. Затем назовите максимальную допустимую давность. Только после этого выбирайте директивы.</p>\n<h2>Свежесть не равна обновлению</h2>\n<p>Свежий ответ кэш может использовать без обращения к origin. Когда срок истёк, ответ становится устаревшим, но это не обязательно означает повторную передачу всего тела. Кэш или браузер отправляет условный запрос с валидатором, если политика требует проверки. При совпадении <code>ETag</code> origin отвечает <code>304 Not Modified</code>. Тело остаётся в кэше, а представление считается подтверждённым. При изменении origin отдаёт <code>200</code> с новым телом и новым валидатором.</p>\n<p><code>no-cache</code> разрешает хранить ответ, но требует проверки перед повторным использованием. <code>no-store</code> запрещает сохранять ответ и применяется, когда само хранение создаёт риск. <code>private</code> запрещает использовать ответ shared cache, но не отменяет хранение в браузере. Эти директивы нельзя заменять друг другом ради «самой свежей страницы».</p>\n<p>Длинный TTL безопасен для файла с версией в URL, а не для любого статического файла. Если сервер всегда отдаёт <code>/assets/app.js</code>, годовой <code>max-age</code> закрепит старые байты. Если имя меняется вместе со сборкой, старый URL становится отдельным ресурсом, а новый HTML получает новый ключ.</p>\n<pre><code>HTTP/1.1 200 OK\nContent-Type: application/json\nCache-Control: public, max-age=30, s-maxage=120\nETag: \"catalog-202-17\"\nVary: Accept-Language\n\n{\"items\":[{\"id\":42,\"name\":\"Example\"}]}</code></pre>\n<p>Пример учебный. Он применим только к публичному каталогу, если язык действительно меняет представление. В нём private cache может считать ответ свежим 30 секунд, а shared cache — 120 секунд: <code>s-maxage</code> переопределяет для него <code>max-age</code>. Оба значения — проектные, их нужно согласовать с допустимой давностью данных. Если цена зависит от пользователя, промокода или cookie, <code>public</code> здесь неверен. Пример не сообщает production-результат и не заменяет проверку CDN.</p>\n<h2>Проверяйте путь ответа, а не только origin</h2>\n<p>У ответа может быть несколько границ: приложение, reverse proxy, CDN и браузер. Origin может прислать правильный заголовок, а промежуточный слой — заменить TTL, изменить ключ или не передать условный запрос. Поэтому один запрос к localhost не подтверждает поведение публичного адреса.</p>\n<pre><code># Снимите обычный ответ и сохраните заголовки.\ncurl -sS -D /tmp/catalog.headers -o /tmp/catalog.body \\\n https://example.test/api/catalog?category=12\n\n# Подставьте ETag из первого ответа.\ncurl -sS -D - -o /dev/null \\\n -H 'If-None-Match: \"catalog-202-17\"' \\\n https://example.test/api/catalog?category=12</code></pre>\n<p>В реальном проекте сравните <code>Cache-Control</code>, <code>ETag</code>, <code>Last-Modified</code>, <code>Vary</code>, <code>Age</code>, статус и тело. Сначала выполните запрос через публичный путь. Затем повторите его на тестовом origin, если такой путь доступен. Разница между ответами указывает на слой, где контракт изменился.</p>\n<figure><img src=\"/assets/editorial/2019/http-cache-response-path-2019.svg\" alt=\"Путь HTTP-ответа через браузер, CDN и origin: HTML перепроверяется, версионированный asset получает новый URL, API имеет короткую свежесть и валидатор\" loading=\"lazy\" /><figcaption>Кэширование требует разных договоров для документа, файла сборки и API. Важен весь путь от запроса до origin.</figcaption></figure>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>После релиза старый JavaScript</td><td>Постоянный URL и долгий TTL</td><td>Сравнить URL asset в новом HTML и <code>Age</code> ответа</td><td>Добавить fingerprint и публиковать новый URL</td></tr><tr><td>Всегда приходит полный ответ</td><td>Нет валидатора или не проходит условный запрос</td><td>Повторить запрос с <code>If-None-Match</code> и проверить статус</td><td>Настроить <code>ETag</code> либо <code>Last-Modified</code>; найти слой, который удаляет заголовок</td></tr><tr><td>Английский запрос получает русский текст</td><td>Ключ не учитывает язык</td><td>Сделать два запроса с разным <code>Accept-Language</code> и сравнить <code>Vary</code></td><td>Добавить вариант в ключ или отключить общий кэш для ответа</td></tr><tr><td>Устаревшая цена после TTL</td><td>Кэш настроен дольше допустимого или edge не применил заголовок origin</td><td>Сравнить <code>Age</code>, <code>Cache-Control</code> и настройки CDN</td><td>Сократить TTL на нужном слое и оставить измеряемый путь обновления</td></tr><tr><td>Данные одной сессии видны другой</td><td>Персональный ответ разрешён shared cache</td><td>Проверить тело и заголовки на двух тестовых сессиях</td><td>Использовать <code>private</code> или <code>no-store</code>; разделить публичную и личную части</td></tr></tbody></table>\n<h2>Порядок действий</h2>\n<ol><li>Выберите один проблемный URL. Зафиксируйте метод, query-параметры и заголовки, которые могут менять ответ.</li><li>Опишите ошибку в терминах пользователя: какая старая версия допустима и сколько времени.</li><li>Разделите HTML, assets и API. Не переносите договор одного типа ресурса на другой.</li><li>Проверьте cache key. Для вариантов используйте <code>Vary</code> или явное правило shared cache; для персональных ответов исключите общее хранение.</li><li>Выберите механизм. Для стабильного URL используйте короткую свежесть и валидатор. Для неизменяемого по URL файла меняйте имя при изменении байтов. Для чувствительных данных запретите хранение, если <code>private</code> недостаточно.</li><li>Снимите обычный ответ и выполните условный запрос. Сохраните статус, тело или факт <code>304</code> и ключевые заголовки.</li><li>Повторите проверку через публичный CDN и после следующего изменения. Очистку кэша используйте только как отдельный способ восстановления, а не как доказательство корректной настройки.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>HTTP-заголовки не управляют всеми промежуточными правилами. CDN может иметь собственный TTL, cache key и исключения для query-параметров. Браузер может использовать историю навигации иначе, чем обычный reload. Nginx применяет <code>add_header</code> с учётом кода ответа и наследования конфигурации; вложенный <code>location</code> может изменить ожидаемый набор заголовков. Это нужно проверять на фактическом ответе.</p>\n<p>Если после изменения конфигурации старый ответ всё ещё приходит, не увеличивайте TTL и не объявляйте инвалидацию успешной. Проверьте, тот ли URL запрашивается, тот ли слой отвечает, не меняется ли cache key, есть ли <code>Age</code> и передаётся ли <code>If-None-Match</code>. Если ответ персональный или входы не известны, остановите общий кэш. Без полного ключа безопасного TTL не существует.</p>\n<h2>Критерий готовности</h2>\n<p>Настройка готова, когда для каждого из трёх типов ресурса есть записанный контракт: URL, входы, допустимая давность, директивы и слой хранения. Для неизменённого ответа условный запрос даёт <code>304</code> или другой явно согласованный результат. При изменении байтов asset HTML получает новый URL, API не смешивает варианты, а персональный ответ не попадает в shared cache. Эти условия проверяются повторяемыми запросами, а не очисткой браузера и не ощущением, что страница «стала быстрее».</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc9111.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9111: HTTP Caching</a> — актуальный стандарт IETF о ключе кэша, свежести, хранении, <code>Vary</code> и директивах; он заменяет RFC 7234.</li><li><a href=\"https://nginx.org/en/docs/http/ngx_http_headers_module.html\" target=\"_blank\" rel=\"noopener noreferrer\">nginx: ngx_http_headers_module</a> — официальное описание <code>add_header</code>, <code>expires</code> и правил наследования.</li></ul>"
|
||
}
|