Zaimplementuj warunkowe GET, wysyłając stabilne walidatory z każdym dokumentem, sprawdzając walidatory zapisane przez klienta i zwracając albo aktualny dokument, albo odpowiedź 304 Not Modified bez treści. Ten poradnik dla Maca pokazuje ten proces na przykładzie syntetycznej strony tekstowej.
Celem jest to, by przy kolejnych żądaniach publicznego dokumentu przesyłać jego zawartość tylko wtedy, gdy jest to potrzebne. Uruchomisz niewielki lokalny serwer w Pythonie, przetestujesz go z poziomu Terminal i zasymulujesz aktualizację dokumentu. Przykład działa lokalnie; jego publikacja to osobne zadanie.
Jak serwer powinien wybierać między 200 a 304?
Na pierwsze żądanie serwer odpowiada 200 OK, przesyła treść dokumentu i jego walidatory. Kolejne żądanie warunkowe zawiera zapisane walidatory. Jeśli dokument się nie zmienił, serwer zwraca 304 Not Modified, dzięki czemu klient może ponownie użyć zapisanej treści. Zmieniona zawartość otrzymuje nową odpowiedź 200. Warunkowe żądania HTTP.
W przypadku istniejącego dokumentu, dla którego w przeciwnym razie zwrócono by 200, zastosuj poniższą kolejność decyzji. If-None-Match ma pierwszeństwo przed If-Modified-Since; nie są to dwa niezależne testy. RFC 9110, pola żądania warunkowego.
| Warunek żądania | Odpowiedź |
|---|---|
| Brak nagłówków warunkowych | 200 z dokumentem |
If-None-Match pasuje do bieżącego tagu |
304 bez treści |
Nagłówek If-None-Match jest obecny, ale nie pasuje |
200, nawet jeśli podana data wskazuje, że nie ma zmian |
Podano tylko prawidłowy If-Modified-Since, a dokument nie jest nowszy |
304 bez treści |
Podano tylko prawidłowy If-Modified-Since, a dokument jest nowszy |
200 z dokumentem |
Sprawdź te warunki po wybraniu dokumentu i ustaleniu jego zwykłej odpowiedzi. Brakujący dokument nadal musi otrzymać zwykłą odpowiedź informującą o braku zasobu. Odpowiedź 304 nie może zawierać treści ani końcowych nagłówków. Musi zawierać odpowiednie metadane pamięci podręcznej, w tym pola Date, ETag, Vary, Cache-Control, Expires i Content-Location, które znalazłyby się w odpowiedzi 200. RFC 9110, ocena warunków wstępnych i wymagania dla 304.
Dobierz walidatory opisujące opublikowany dokument
ETag to ujęty w cudzysłowy identyfikator wersji, na przykład "doc-v1". Sposób jego generowania zależy od implementacji: może to być identyfikator rewizji lub skrót zawartości. Zachowaj jego wartość dla tej samej reprezentacji i zmień ją, gdy reprezentacja ulegnie zmianie. Przykład wykorzystuje dwie jawnie określone rewizje o ustalonej zawartości. Nagłówek ETag.
Silny tag identyfikuje konkretną reprezentację; słaby tag ma prefiks W/ i może opisywać semantycznie równoważną zawartość, której bajty są różne. Porównanie If-None-Match w warunkowych żądaniach GET odbywa się słabo, więc W/"doc-v1" pasuje do "doc-v1". Żądania mogą zawierać także listę tagów albo samodzielny symbol wieloznaczny *. RFC 9110, If-None-Match.
Last-Modified informuje, kiedy dokument uległ zmianie. Używaj konsekwentnie ustalanego czasu modyfikacji, uwzględniającego odpowiednie składniki dokumentu, a nie czasu każdego żądania. Nie może on przypadać później niż data odpowiedzi w polu Date. RFC 9110, generowanie Last-Modified.
Przykład przypisuje dwóm syntetycznym rewizjom celowo poglądowe czasy publikacji: południe GMT 1 i 2 stycznia 2024 roku. Te daty to dane testowe. W rzeczywistym procesie publikacji korzystaj z aktualizowanych metadanych publikacji. Last-Modified jest mniej precyzyjny od ETag przy wykrywaniu różnic w zawartości i stanowi przydatne rozwiązanie zapasowe. Nagłówek Last-Modified.
Ustal osobno zasady pamięci podręcznej
Walidatory informują, czy zapisana kopia nadal jest aktualna. Zasady świeżości określają, kiedy pamięć podręczna musi zapytać serwer. Ten poradnik wysyła Cache-Control: no-cache, co pozwala przechowywać odpowiedź, ale wymaga jej pomyślnej walidacji przed ponownym użyciem. Dzięki temu łatwo zaobserwować zachowanie przy kolejnych żądaniach. no-store zabrania przechowywania, a max-age pozwala ponownie użyć odpowiedzi, dopóki jest świeża. RFC 9111, dyrektywy pamięci podręcznej odpowiedzi.
W przypadku publicznego dokumentu, w którym można zaakceptować krótkie opóźnienie, zanim aktualizacja będzie widoczna, może sprawdzić się zasada taka jak max-age=60. To poglądowa decyzja redakcyjna: dobierz przedział do wymagań dotyczących aktualizacji dokumentu. Świeżą odpowiedź z pamięci podręcznej można ponownie wykorzystać bez kontaktowania się z serwerem, więc każda kolejna wizyta nie musi powodować 304. RFC 9111, świeżość.
Uruchom kompletny lokalny przykład na Macu
Otwórz Terminal i sprawdź wymagania wstępne, wykonując poniższe polecenia. Użyj Pythona 3.9 lub nowszego, ponieważ program korzysta z str.removeprefix. Jeśli python3 jest niedostępne, zainstaluj Pythona 3 przed kontynuowaniem. Nie są potrzebne żadne zewnętrzne pakiety Pythona.
python3 --version
curl --version
W nowym folderze roboczym zapisz poniższy kompletny program jako server.py, używając edytora zwykłego tekstu. Obsługuje on tylko /page.txt, implementuje GET i HEAD, a rewizję dokumentu wybiera na podstawie argumentu wiersza poleceń. BaseHTTPRequestHandler z Pythona kieruje żądania do odpowiednich metod i udostępnia funkcje pomocnicze do zapisywania odpowiedzi. Dokumentacja serwera HTTP w Pythonie.
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()
To lokalna demonstracja dwóch wymienionych walidatorów, a nie kompletny stos do obsługi HTTP. Wykorzystywana konwersja dat korzysta z liberalnego parsera dat e-mailowych z Pythona; wdrożenie produkcyjne powinno używać walidacji dat właściwej dla HTTP i obsługi żądań warunkowych. Python opisuje akceptowane dane wejściowe parsera i zgłaszane przez niego wyjątki. Narzędzia do obsługi dat w Pythonie.
W Terminal przejdź do folderu zawierającego server.py i uruchom rewizję 1. Pozostaw to okno otwarte. Jeśli port 8000 jest już zajęty, wybierz inny dostępny port w programie i używaj go we wszystkich testowych adresach URL.
python3 server.py 1
Przetestuj niezmienioną i zmienioną zawartość oraz pierwszeństwo
Otwórz drugie okno Terminal. Poniższe żądania wyświetlają nagłówki odpowiedzi i każdą zwróconą treść. Opcja -i dołącza nagłówki, -H podaje nagłówek żądania, a --max-time ogranicza czas przesyłania. Podręcznik curl.
Uzyskaj początkową odpowiedź
curl --max-time 5 -sS -i http://127.0.0.1:8000/page.txt
Oczekuj 200 OK, tekstu syntetycznej strony, ETag: "doc-v1" i Last-Modified: Mon, 01 Jan 2024 12:00:00 GMT. Stosując tę procedurę do innego dokumentu, zanotuj faktycznie zwrócone wartości; walidatory dotyczą konkretnej odpowiedzi.
Ponownie zweryfikuj przy użyciu każdego walidatora
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
Oba żądania powinny zwrócić 304 Not Modified bez tekstu strony. Odpowiedź zachowuje ETag i Cache-Control; funkcja pomocnicza Pythona dodaje Date. Powtórzenie pierwszego żądania z W/"doc-v1" również powinno zwrócić 304. Przykład analizuje tagi ujęte w cudzysłowy, zamiast dzielić je bezwarunkowo po przecinkach, ponieważ przecinek może wystąpić wewnątrz tagu.
Sprawdź, czy ETag ma pierwszeństwo
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
Oczekuj 200 OK z dokumentem. Tag nie pasuje, więc późniejsza data nie powinna wywołać 304. To celowo sprzeczne żądanie pozwala wykryć implementację, która błędnie łączy oba testy.
Zasymuluj opublikowanie kolejnej rewizji
W oknie serwera naciśnij Control-C, a następnie uruchom rewizję 2. Nie zmieniaj adresu URL żądania.
python3 server.py 2
Powtórz dwa wcześniejsze żądania ponownej walidacji, zawierające walidatory rewizji 1. Oba powinny teraz zwrócić 200, zaktualizowane zdanie wspominające trójkąt, ETag: "doc-v2" i datę modyfikacji 2 stycznia. Powtórzenie żądania z If-None-Match: "doc-v2" powinno zwrócić 304.
Wyniki składają się na sekwencję testową: początkowe pobranie, walidacja niezmienionej treści, sprzeczne walidatory, zmieniony dokument, a następnie walidacja niezmienionej treści nowej rewizji. Przykład sprawdzono w 24 lokalnych testach obejmujących obie rewizje, słabe tagi, listy tagów, dopasowanie symbolu wieloznacznego, HEAD i brakujące ścieżki.
Dlaczego odpowiedzi przeglądarki i Google mogą się różnić
Przeglądarki zwykle obsługują walidatory HTTP za pośrednictwem swojej pamięci podręcznej. Algorytm WHATWG Fetch dodaje zapisane walidatory podczas ponownej walidacji, a po pomyślnym 304 aktualizuje nagłówki zapisanej odpowiedzi i zwraca zapisaną odpowiedź. W rezultacie JavaScript aplikacji może otrzymać użyteczną odpowiedź 200 wraz z treścią dokumentu, mimo że ponowna walidacja w sieci otrzymała 304. Standard Fetch, pobieranie przez HTTP z pamięci podręcznej lub sieci.
Diagnozując zachowanie serwera, zacznij od jawnych żądań curl podanych wyżej. Ręczne ustawianie nagłówków warunkowych w JavaScripcie przeglądarki zmienia test: Fetch przełącza żądanie z domyślną pamięcią podręczną, zawierające te nagłówki, na no-store. Podczas testowania typowej ponownej walidacji przeglądarki pozwól jej samodzielnie zarządzać pamięcią podręczną. Standard Fetch, tryb pamięci podręcznej żądania.
Infrastruktura Google do indeksowania obsługuje obie pary walidatorów i zaleca ETag, ponieważ pozwala uniknąć problemów z formatowaniem dat. Dokumentacja informuje też, że ta infrastruktura nie obsługuje innych dyrektyw pamięci podręcznej HTTP. Zasady pamięci podręcznej przeglądarki dobieraj niezależnie od oczekiwań dotyczących ponownego indeksowania przez Google. Przegląd robotów indeksujących i pobierających dane Google.
Google nie wysyła nagłówków warunkowych przy każdej próbie indeksowania. Dokumentacja wyszukiwarki wyjaśnia, że 304 pozwala ponownie wykorzystać wcześniej pobraną treść, a nowsza treść może otrzymać 200 ze zaktualizowaną zawartością. Zwykłe żądania bez warunków nadal powinny działać; żądanie robota bez walidatorów nie świadczy o błędzie implementacji. Rozwiązywanie problemów z błędami indeksowania w Google Search.
Zanim zastosujesz ten schemat do opublikowanego dokumentu, powtórz sekwencję testową dla jego rzeczywistego adresu URL. Sprawdź, czy niezmienione rewizje zachowują swoje walidatory, edycje je aktualizują, a 304 nie zawiera treści dokumentu. Dzięki temu dokument będzie prawidłowo ponownie wykorzystywany, a zmienioną zawartość nadal będzie można pobrać.
Źródła
- RFC 9110: Semantyka HTTP — IETF / RFC Editor.
- RFC 9111: Buforowanie HTTP — IETF / RFC Editor.
- Standard Fetch — WHATWG.
- Warunkowe żądania HTTP — MDN Web Docs.
- Nagłówek ETag — MDN Web Docs.
- Nagłówek Last-Modified — MDN Web Docs.
- http.server — serwery HTTP — Python Software Foundation.
- email.utils: różne narzędzia — Python Software Foundation.
- curl — jak używać — projekt curl.
- Przegląd robotów indeksujących i pobierających dane Google (agentów użytkownika) — Google.
- Rozwiązywanie problemów z błędami indeksowania w Google Search — Google Search Central.