Варианты кешируемого ответа
В предыдущей части мы проверяли JSON курса через ETag. Теперь добавим второй важный вопрос: какую сохранённую копию вообще можно использовать для нынешнего запроса? Совпадение пути ещё не гарантирует одинаковый формат, язык или пользователя. Кеш полезен только тогда, когда повторный ответ соответствует принятому договору.
В этом уроке сохраним публичное чтение каталога и введём отдельный персональный ресурс. Результат — вы сможете объяснить, почему HTML и JSON нельзя смешать в одной ячейке кеша и почему профиль редактора не должен попасть другому читателю. Примеры остаются авторской моделью catalog.example, без сервера, измерений и фактической передачи сообщений.
Публичный курс зависит от формата
Курс c001 в снимке s1 имеет название «Современный JavaScript», направление frontend и 40 уроков. Его общедоступное представление одинаково для всех читателей. Запрос с Accept JSON возвращает следующий смысловой ответ:
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "c001-json-r1"
Cache-Control: public, no-cache
Vary: Accept
{"id":"c001","title":"Современный JavaScript","topic":"frontend","lessons":40}
Поле Vary связывает выбор ответа с полем Accept запроса. Кеш, сохранивший JSON, не должен без подходящего сопоставления вернуть его клиенту, который просит только HTML. Адрес ресурса прежний, но вариант представления другой. HTML получает отдельную метку "c001-html-r1", поскольку его байты не совпадают с JSON.
Значение public указывает на возможность общего хранения при соблюдении остальных правил. no-cache разрешает сохранение, но требует проверки перед повторным использованием. Это не противоречие: одно решение касается допустимости хранения, другое — необходимости подтверждения. Если заменить no-cache на no-store, сохранение ответа HTTP-кешами уже не входит в разрешённое поведение.
На уровне реализации ключ обычно начинается с метода и целевого URI, а Vary добавляет сведения о выборе. Редактор не должен считать, что любые параметры URL автоматически игнорируются или что кеш сам угадает семантику фильтра. Разные query могут обозначать разные представления коллекции. Выбор сохранённого ответа по Vary.
Условная проверка относится к варианту
Если клиент сохранил JSON, он посылает If-None-Match с JSON-меткой и прежним Accept. При совпадении получается 304 без тела. Если он сменил Accept на HTML, прежняя JSON-метка не подтверждает выбранный HTML. Обычный ответ 200 в таком случае не доказывает, что предметные данные курса изменились: изменился вариант чтения.
Такое различие становится особенно важным при обновлении шаблона. Можно изменить только HTML-разметку, не трогая JSON. Сильный ETag HTML изменится, а JSON останется прежним. Внутренняя версия записи базы сама по себе не даёт подходящий сильный валидатор всех возможных форм.
Мы пока не локализуем названия и не включаем сжатие в этот учебный обмен. Если добавить выбор по Accept-Language или Accept-Encoding, понадобится одновременно определить Vary и правильные валидаторы. Нельзя оставить прежнюю метку «версии курса» и считать, что новые байты не имеют значения. У сильного валидатора есть обещание о выбранном представлении.
Профиль редактора — другой ресурс
Публичное чтение не должно тайно добавлять роль или служебные заметки после обнаружения сессии. Для личных сведений введём /api/me. Считаем, что Alice уже подтверждена сервером как пользователь u17. Механизм сессии разберём в следующих уроках, здесь нужен только ожидаемый результат:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: private, no-store
{"id":"u17","role":"editor","topics":["frontend"]}
Такой ответ зависит от личности и не становится публичным только из-за статуса 200. private запрещает общий кеш, no-store задаёт более строгое правило хранения участвующими HTTP-кешами. Сочетание явно выражает цель примера: персональные данные не должны сохраняться для повторной общей выдачи.
Сам no-store не стирает уже показанный экран, журнал приложения или пользовательский снимок. Он не проверяет права и не отзывает сессию. Сервер обязан устанавливать личность на каждом новом защищённом обращении независимо от политики кеша. Иначе красивый заголовок будет лишь сопровождать неправильно раскрытые данные.
Иногда предлагают поставить Vary: Cookie и считать личный ответ безопасным для общего хранения. Это неудачная замена явному решению о приватности: Cookie содержит множество значений, создаёт огромное число вариантов и не выражает жизненный цикл прав. Для нашего профиля выбран no-store. Публичный курс, напротив, сохраняет одинаковые поля независимо от наличия cookie.
Cookie не отменяет правила автоматически
Наличие Set-Cookie само по себе не является универсальным запретом кеширования HTTP. Поэтому обработчик входа и персональный ответ должны устанавливать правильную политику явно. Если ответ выдал сессию и был ошибочно объявлен публичным, нельзя рассчитывать, что все посредники исправят противоречие по своей догадке.
Запрос с Authorization имеет специальные правила общего кеширования. Не следует переносить их механически на Cookie: это разные поля с разными механизмами. Наша простая модель избегает скрытого персонального представления публичного курса. Защищённые профиль, CSRF-токен, статус личной задачи и файл получают no-store.
Такое разделение полезно и для сопровождения. Новый разработчик знает, что /api/courses/c001 содержит только общедоступные свойства. Он не должен искать, какое поле случайно появляется у администратора. Редакторский интерфейс получает возможности через отдельные защищённые обращения, а не через скрытый дополнительный объект внутри публичного кеша.
Когда CORS добавляет ещё одно измерение
Позже появится доверенный интерфейс на https://editor.catalog.example. Он имеет другой origin и может нуждаться в разрешающих полях CORS. Если сервер выбирает Access-Control-Allow-Origin по Origin запроса, это поле выбора также нужно отразить в Vary, сохраняя Accept для формата.
Vary: Accept, Origin
Access-Control-Allow-Origin: https://editor.catalog.example
Access-Control-Allow-Credentials: true
Этот фрагмент заменяет Vary соответствующего ответа и добавляет разрешение конкретному интерфейсу. Он не является разрешением любому сайту и не заменяет проверку пользователя. Сам ресурс курса остаётся публичным, но заголовки допустимого браузерного чтения выбираются по контексту запроса.
Если кеш сохранит разрешение одного origin без учёта выбора, другой клиент способен получить неправильные CORS-поля. Тело может быть тем же, а поведение браузера — другим. Поэтому понятие представления и метаданные ответа нужно рассматривать вместе, а не проверять только JSON.
Политика должна соответствовать изменению
Кеширование хорошо работает, когда процесс обновления сохраняет обещанные правила. Неизменяемый ресурс с долгим сроком свежести требует устойчивых байтов по своему адресу. Изменяемый курс в нашей модели проверяется валидатором. Личный профиль требует новых прав и не идёт в общий кеш. Эти три случая нельзя закрыть одним максимальным сроком для всех путей.
Для ручного разбора выпишите адрес, формат, зависимость от пользователя и разрешённый способ повторного использования. Затем измените только один контекст: Accept, личность либо Origin. Если ответ должен поменяться, это должно быть видно в его договоре. Такое рассуждение полезнее предположения, что «кеш сам разберётся».
При диагностике различайте сохранённую публичную копию и актуальную серверную запись. Если интерфейс показывает старое title, нужно узнать, какой вариант и какие метаданные он использовал. Само наличие ETag в предыдущем ответе не означает, что программа отправила условный запрос или дождалась его результата. Политика кеширования работает только вместе с корректным клиентским поведением и фактической конфигурацией посредников, которые здесь не наблюдались.
Теперь публичная и персональная выдача разделены. Мы по-прежнему не заявляем фактическую скорость или безопасность работающей реализации: это требования к ней. Следующий урок объяснит, как браузер хранит cookie и почему область её отправки не совпадает с правом читать или изменять курс.