Реализуйте условный GET: отправляйте стабильные валидаторы вместе с каждым документом, проверяйте сохранённые валидаторы клиента и возвращайте либо актуальный документ, либо ответ 304 Not Modified без тела. В этом руководстве для Mac процесс показан на искусственной текстовой странице.
Задача — обеспечить передачу содержимого общедоступного документа при повторных запросах только при необходимости. Вы запустите небольшой локальный сервер Python, проверите его из Terminal и смоделируете обновление документа. Пример работает локально; публикация — отдельная задача.
Как серверу выбрать между 200 и 304?
На первоначальный запрос сервер отвечает 200 OK, телом документа и его валидаторами. При следующем условном запросе клиент отправляет сохранённые валидаторы обратно. Если документ не изменился, сервер возвращает 304 Not Modified, позволяя клиенту повторно использовать сохранённое тело. Если содержимое изменилось, сервер возвращает ответ 200. Условные запросы HTTP.
Для существующего документа, который в противном случае вернул бы 200, примените следующую последовательность проверки. If-None-Match имеет приоритет над If-Modified-Since; это не две независимые проверки. RFC 9110, поля условного запроса.
| Условие запроса | Ответ |
|---|---|
| Нет условных заголовков | 200 с документом |
If-None-Match совпадает с текущим тегом |
304 без тела |
Заголовок If-None-Match присутствует, но не совпадает |
200, даже если указанная дата предполагает отсутствие изменений |
Указан только корректный If-Modified-Since, и документ не новее этой даты |
304 без тела |
Указан только корректный If-Modified-Since, и документ новее этой даты |
200 с документом |
Проверяйте эти условия после выбора документа и подготовки обычного ответа для него. Отсутствующий документ по-прежнему должен вызывать обычный ответ об отсутствии ресурса. Ответ 304 не может содержать содержимое или завершающие поля. В нём должны присутствовать применимые метаданные кэширования, в том числе поля Date, ETag, Vary, Cache-Control, Expires и Content-Location, которые сопровождали бы ответ 200. RFC 9110, проверка предварительных условий и требования к 304.
Выбирайте валидаторы, описывающие опубликованный документ
ETag — это заключённый в кавычки идентификатор версии, например "doc-v1". Способ его формирования зависит от реализации: подойдут идентификатор редакции или хеш содержимого. Для одного и того же представления сохраняйте его неизменным, а при изменении представления меняйте. В примере используются две заданные вручную редакции с неизменным содержимым. Заголовок ETag.
Сильный тег идентифицирует конкретное представление; слабый тег имеет префикс W/ и может описывать семантически эквивалентное содержимое с отличающимися байтами. При сравнении If-None-Match для условного GET используется слабое сравнение, поэтому W/"doc-v1" соответствует "doc-v1". Запросы также могут содержать список тегов или отдельный подстановочный символ *. RFC 9110, If-None-Match.
Last-Modified указывает, когда изменился документ. Используйте последовательно определяемое время изменения с учётом всех относящихся к документу компонентов, а не время каждого запроса. Оно не должно быть позднее даты ответа в поле Date. RFC 9110, формирование Last-Modified.
В примере для двух искусственных редакций указаны специально подобранные иллюстративные даты публикации: 1 и 2 января 2024 года, 12:00 GMT. Эти даты — тестовые данные. В реальном процессе публикации используйте поддерживаемые в актуальном состоянии метаданные публикации. Last-Modified менее точен, чем ETag, при обнаружении различий в содержимом, но служит полезным резервным вариантом. Заголовок Last-Modified.
Выберите политику кэширования отдельно
Валидаторы показывают, совпадает ли сохранённая копия с оригиналом. Политика свежести определяет, когда кэшу нужно отправить запрос. В этом руководстве используется Cache-Control: no-cache: этот заголовок разрешает хранение, но требует успешной проверки перед повторным использованием. Благодаря этому поведение при повторных запросах легко наблюдать. no-store запрещает хранение, а max-age разрешает повторное использование, пока ответ остаётся свежим. RFC 9111, директивы кэширования ответа.
Для общедоступного документа, в случае которого допустима небольшая задержка перед отображением обновления, может подойти политика вроде max-age=60. Это иллюстративный редакционный выбор: выбирайте интервал с учётом требований к обновлению документа. Свежий ответ из кэша можно использовать повторно без обращения к серверу, поэтому каждый повторный просмотр не обязательно вызывает 304. RFC 9111, свежесть.
Запустите полный локальный пример на Mac
Откройте Terminal и проверьте требования с помощью следующих команд. Используйте Python 3.9 или новее, поскольку программа использует str.removeprefix. Если команда python3 недоступна, перед продолжением установите Python 3. Сторонние пакеты Python не нужны.
python3 --version
curl --version
В новой рабочей папке сохраните следующую полную программу под именем server.py с помощью обычного текстового редактора кода. Она обслуживает только /page.txt, реализует GET и HEAD, а редакцию документа выбирает по аргументу командной строки. Класс Python BaseHTTPRequestHandler направляет запросы соответствующим методам и предоставляет вспомогательные функции для отправки ответов. Документация Python по HTTP-серверу.
import argparse
import re
from datetime import datetime, timezone
from email.utils import format_datetime, parsedate_to_datetime
from http.server import BaseHTTPRequestHandler, HTTPServer
parser = argparse.ArgumentParser()
parser.add_argument("version", type=int, choices=(1, 2))
args = parser.parse_args()
BODY = {
1: b"Paper Shapes\nA circle and a square.\n",
2: b"Paper Shapes\nA circle, a square, and a triangle.\n",
}[args.version]
ETAG = '"doc-v%d"' % args.version
MODIFIED = datetime(2024, 1, args.version, 12, tzinfo=timezone.utc)
LAST_MODIFIED = format_datetime(MODIFIED, usegmt=True)
TOKEN = r'(?:W/)?"[\x21\x23-\x7e\x80-\xff]*"'
TAG_LIST = re.compile(rf'[ \t]*(?:{TOKEN})?(?:[ \t]*,[ \t]*(?:{TOKEN})?)*[ \t]*')
def tag_matches(value):
if value.strip() == "*":
return True
if not TAG_LIST.fullmatch(value):
return False
return any(tag.removeprefix("W/") == ETAG
for tag in re.findall(TOKEN, value))
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.serve_document()
def do_HEAD(self):
self.serve_document()
def serve_document(self):
if self.path != "/page.txt":
self.send_error(404)
return
not_modified = False
tags = self.headers.get_all("If-None-Match")
if tags is not None:
not_modified = tag_matches(",".join(tags))
else:
dates = self.headers.get_all("If-Modified-Since", [])
if len(dates) == 1:
try:
since = parsedate_to_datetime(dates[0])
if since.tzinfo is None:
since = since.replace(tzinfo=timezone.utc)
not_modified = MODIFIED <= since
except (TypeError, ValueError, OverflowError):
pass
self.send_response(304 if not_modified else 200)
self.send_header("ETag", ETAG)
self.send_header("Cache-Control", "no-cache")
if not not_modified:
self.send_header("Last-Modified", LAST_MODIFIED)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(BODY)))
self.end_headers()
if not not_modified and self.command == "GET":
self.wfile.write(BODY)
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()
Это локальная демонстрация двух указанных валидаторов, а не полноценный стек для обслуживания HTTP. Для преобразования даты используется универсальный парсер дат из email-модуля Python, принимающий множество форматов; в развёрнутой реализации следует применять проверку дат и обработку условных запросов, специфичные для HTTP. В документации Python описаны допустимые входные данные этого парсера и возникающие исключения. Утилиты Python для работы с датами электронной почты.
В Terminal перейдите в папку с файлом server.py и запустите редакцию 1. Оставьте это окно открытым. Если порт 8000 уже занят, выберите другой свободный порт в программе и укажите его во всех тестовых URL.
python3 server.py 1
Проверьте неизменившееся и изменившееся содержимое, а также приоритет валидаторов
Откройте ещё одно окно Terminal. Следующие запросы выводят заголовки ответа и возвращаемое тело, если оно есть. Параметр -i включает заголовки, -H задаёт заголовок запроса, а --max-time ограничивает время передачи. Руководство по curl.
Получите первоначальный ответ
curl --max-time 5 -sS -i http://127.0.0.1:8000/page.txt
Ожидайте 200 OK, текст искусственной страницы, ETag: "doc-v1" и Last-Modified: Mon, 01 Jan 2024 12:00:00 GMT. Применяя эту процедуру к другому документу, запишите фактически полученные значения: валидаторы относятся к конкретному ответу.
Повторно проверьте каждый валидатор
curl --max-time 5 -sS -i \
-H 'If-None-Match: "doc-v1"' \
http://127.0.0.1:8000/page.txt
curl --max-time 5 -sS -i \
-H 'If-Modified-Since: Mon, 01 Jan 2024 12:00:00 GMT' \
http://127.0.0.1:8000/page.txt
Оба запроса должны вернуть 304 Not Modified без текста страницы. В ответе сохраняются ETag и Cache-Control; вспомогательная функция Python добавляет Date. Повторный запрос с W/"doc-v1" также должен вернуть 304. В примере разбираются теги в кавычках, а не выполняется простое разделение по запятым, поскольку запятая может присутствовать внутри тега.
Убедитесь, что ETag имеет приоритет
curl --max-time 5 -sS -i \
-H 'If-None-Match: "older-revision"' \
-H 'If-Modified-Since: Wed, 03 Jan 2024 12:00:00 GMT' \
http://127.0.0.1:8000/page.txt
Ожидайте 200 OK с документом. Тег не совпадает, поэтому более поздняя дата не должна вызывать ответ 304. Этот намеренно противоречивый запрос позволяет обнаружить реализацию, в которой две проверки ошибочно объединены.
Смоделируйте публикацию следующей редакции
В окне сервера нажмите Control-C, затем запустите редакцию 2. URL запроса оставьте прежним.
python3 server.py 2
Повторите два предыдущих запроса на повторную проверку с валидаторами редакции 1. Теперь оба должны вернуть 200, обновлённое предложение с упоминанием треугольника, ETag: "doc-v2" и дату изменения 2 января. Запрос с If-None-Match: "doc-v2" должен вернуть 304.
Эти результаты задают последовательность проверки: первоначальная загрузка, проверка неизменившегося документа, противоречащие друг другу валидаторы, изменение документа, затем проверка неизменившейся новой редакции. Для примера было выполнено 24 локальные проверки обеих редакций, в том числе со слабыми тегами, списками тегов, подстановочным символом, HEAD и отсутствующими путями.
Почему запросы браузера и Google могут выглядеть иначе
Браузеры обычно управляют HTTP-валидаторами с помощью своего кэша. Алгоритм WHATWG Fetch при повторной проверке добавляет сохранённые валидаторы, а после успешного ответа 304 обновляет заголовки сохранённого ответа и возвращает сохранённый ответ. В результате JavaScript приложения может получить пригодный для использования ответ 200 и тело документа, хотя при повторной проверке по сети был получен ответ 304. Стандарт Fetch, получение данных через HTTP-сеть или кэш.
При диагностике поведения сервера начните с явных запросов curl выше. Если вручную задавать условные заголовки в JavaScript браузера, условия проверки изменятся: Fetch переключает запрос с режимом кэша по умолчанию, содержащий такие заголовки, на no-store. Для проверки обычной повторной валидации браузера позвольте ему самому управлять кэшем. Стандарт Fetch, режим кэширования запроса.
Инфраструктура Google для сканирования поддерживает обе пары валидаторов и рекомендует ETag, поскольку с ним не возникает проблем формата даты. В документации Google также указано, что эта инфраструктура не поддерживает другие директивы кэширования HTTP. Выбирайте политику кэширования браузера отдельно от ожиданий относительно поведения Google при повторном сканировании. Обзор поисковых роботов и сборщиков данных Google.
Google отправляет условные заголовки не при каждой попытке сканирования. В документации Google Поиска объясняется, что 304 позволяет повторно использовать ранее просканированное содержимое, а для более нового содержимого можно получить 200 с обновлённым телом. Не отключайте обычную обработку безусловных запросов: если запрос от поискового робота не содержит валидаторов, это не означает, что реализация работает неправильно. Устранение ошибок сканирования Google Поиском.
Прежде чем применять этот способ к опубликованному документу, повторите последовательность проверок для его фактического URL. Убедитесь, что валидаторы неизменившихся редакций сохраняются, при редактировании обновляются, а ответ 304 не содержит тела документа. Результат — корректное повторное использование документа при сохранении возможности получать изменившееся содержимое.
Источники
- RFC 9110: Семантика HTTP — IETF / редактор RFC.
- RFC 9111: Кэширование HTTP — IETF / редактор RFC.
- Стандарт Fetch — WHATWG.
- Условные запросы HTTP — MDN Web Docs.
- Заголовок ETag — MDN Web Docs.
- Заголовок Last-Modified — MDN Web Docs.
- http.server — HTTP-серверы — Фонд Python.
- email.utils: Различные утилиты — Фонд Python.
- curl — Как пользоваться — проект curl.
- Обзор поисковых роботов и сборщиков данных Google (агентов пользователя) — Google.
- Устранение ошибок сканирования Google Поиском — Google Search Central.