Implementeer voorwaardelijke GET door stabiele validators met elk document mee te sturen, de door de client opgeslagen validators te controleren en vervolgens het actuele document of een 304 Not Modified zonder body terug te sturen. Deze Mac-tutorial demonstreert het proces met een synthetische tekstpagina.

De taak is om ervoor te zorgen dat herhaalde verzoeken om een openbaar document de inhoud alleen overdragen wanneer dat nodig is. Je start een kleine lokale Python-server, test deze vanuit Terminal en simuleert een documentupdate. Het voorbeeld blijft lokaal; het publiceren ervan is een aparte taak.

Hoe kiest de server tussen 200 en 304?

Een eerste verzoek krijgt 200 OK, de documentbody en de validators. Een later voorwaardelijk verzoek stuurt de opgeslagen validators terug. Als het document ongewijzigd is, retourneert de server 304 Not Modified, zodat de client de opgeslagen body opnieuw kan gebruiken. Gewijzigde inhoud krijgt een nieuwe 200-reactie. Voorwaardelijke HTTP-verzoeken.

Pas voor een bestaand document dat anders 200 zou retourneren, de volgende beslisvolgorde toe. If-None-Match heeft voorrang op If-Modified-Since; dit zijn geen twee onafhankelijke controles. RFC 9110, velden voor voorwaardelijke verzoeken.

Verzoekvoorwaarde Reactie
Geen voorwaardelijke headers 200 met het document
If-None-Match komt overeen met de huidige tag 304, zonder body
If-None-Match is aanwezig maar komt niet overeen 200, zelfs als een opgegeven datum op geen wijzigingen wijst
Alleen een geldige If-Modified-Since is opgegeven en het document is niet nieuwer 304, zonder body
Alleen een geldige If-Modified-Since is opgegeven en het document is nieuwer 200 met het document

Evalueer deze voorwaarden nadat je het document hebt geselecteerd en de normale reactie hebt vastgesteld. Een ontbrekend document moet nog steeds de normale reactie voor een ontbrekende resource opleveren. Een 304 mag geen inhoud of trailers bevatten. De reactie moet de toepasselijke cachemetadata bevatten, waaronder de velden Date, ETag, Vary, Cache-Control, Expires en Content-Location die ook bij de reactie 200 zouden horen. RFC 9110, evaluatie van precondities en vereisten voor 304.

Kies validators die het gepubliceerde document beschrijven

Een ETag is een versieaanduiding tussen aanhalingstekens, zoals "doc-v1". De methode om deze te genereren is implementatieafhankelijk: een revisieaanduiding of een contenthash kan werken. Houd de tag stabiel zolang dezelfde representatie wordt gebruikt en verander hem wanneer die representatie verandert. Het voorbeeld gebruikt twee expliciete revisies waarvan de inhoud vaststaat. ETag-header.

Een sterke tag identificeert een specifieke representatie; een zwakke tag heeft het voorvoegsel W/ en kan inhoud beschrijven die semantisch gelijkwaardig is, maar waarvan de bytes verschillen. Voorwaardelijke GET-vergelijkingen met If-None-Match gebruiken een zwakke vergelijking, dus W/"doc-v1" komt overeen met "doc-v1". Verzoeken kunnen ook een lijst met tags bevatten, of de zelfstandige joker *. RFC 9110, If-None-Match.

Last-Modified beschrijft wanneer het document is gewijzigd. Gebruik een consistent bepaalde wijzigingstijd waarin de relevante documentonderdelen zijn meegenomen, in plaats van het tijdstip van elk verzoek. Deze tijd mag niet later zijn dan de Date van de reactie. RFC 9110, generatie van Last-Modified.

Het voorbeeld kent de twee synthetische revisies bewust illustratieve publicatietijden toe: 12.00 uur GMT op 1 en 2 januari 2024. Die datums zijn testgegevens. Gebruik in een echte publicatieworkflow bijgehouden publicatiemetadata. Last-Modified is minder nauwkeurig dan een ETag bij het detecteren van inhoudelijke verschillen en biedt een nuttige terugvaloptie. Last-Modified-header.

Kies afzonderlijk een cachebeleid

Validators geven aan of een opgeslagen kopie nog overeenkomt. Het versheidsbeleid bepaalt wanneer een cache navraag moet doen. Deze tutorial stuurt Cache-Control: no-cache, waarmee opslag is toegestaan, maar de kopie vóór hergebruik met succes moet worden gevalideerd. Daardoor is het gedrag bij herhaalde verzoeken eenvoudig te zien. no-store verbiedt opslag, terwijl max-age hergebruik toestaat zolang een reactie vers is. RFC 9111, cache-instructies voor reacties.

Voor een openbaar document waarbij een korte vertraging voordat een update zichtbaar wordt aanvaardbaar is, kan een beleid zoals max-age=60 geschikt zijn. Dat is een illustratieve redactionele keuze: kies het interval op basis van de updatevereisten van het document. Een vers antwoord uit de cache kan opnieuw worden gebruikt zonder contact met de server, dus niet elk herhaald bezoek hoeft een 304 op te leveren. RFC 9111, versheid.

Voer een volledig lokaal voorbeeld uit op je Mac

Open Terminal en controleer met de volgende opdrachten of aan de vereisten is voldaan. Gebruik Python 3.9 of nieuwer, omdat het programma str.removeprefix gebruikt. Als python3 niet beschikbaar is, installeer dan eerst Python 3. Er zijn geen Python-pakketten van derden nodig.

python3 --version
curl --version

Sla in een nieuwe werkmap het volgende volledige programma op als server.py met een editor voor platte tekst. Het serveert alleen /page.txt, implementeert GET en HEAD en kiest een documentrevisie op basis van het opdrachtregelargument. Pythons BaseHTTPRequestHandler stuurt verzoeken door naar de bijbehorende methoden en biedt hulpmethoden om reacties te schrijven. Documentatie van Python HTTP-server.

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()

Dit is een lokale demonstratie van de twee genoemde validators, geen volledige HTTP-serveerstack. Voor de datumconversie gebruikt het programma Pythons tolerante parser voor e-maildatums; een geïmplementeerde productieversie moet HTTP-specifieke datumvalidatie en verwerking van voorwaardelijke verzoeken gebruiken. Python documenteert welke invoer die parser accepteert en welke uitzonderingen kunnen optreden. Datumhulpmiddelen van Python email.

Ga in Terminal naar de map met server.py en start revisie 1. Laat dat venster actief. Als poort 8000 al in gebruik is, kies dan een andere beschikbare poort in het programma en gebruik diezelfde poort in elke test-URL.

python3 server.py 1

Test ongewijzigde inhoud, gewijzigde inhoud en voorrang

Open een ander Terminal-venster. Met de volgende verzoeken worden de antwoordheaders en een eventuele geretourneerde body weergegeven. De optie -i voegt headers toe, -H geeft een verzoekheader op en --max-time begrenst elke overdracht. Curl-handleiding.

Stel de eerste reactie vast

curl --max-time 5 -sS -i http://127.0.0.1:8000/page.txt

Verwacht 200 OK, de tekst van de synthetische pagina, ETag: "doc-v1" en Last-Modified: Mon, 01 Jan 2024 12:00:00 GMT. Noteer de waarden die daadwerkelijk worden geretourneerd wanneer je deze procedure op een ander document toepast; validators horen bij die specifieke reactie.

Valideer opnieuw met elke validator

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

Beide verzoeken moeten 304 Not Modified retourneren zonder de pagin tekst. De reactie behoudt ETag en Cache-Control; de reactiehelper van Python voegt Date toe. Als je het eerste verzoek herhaalt met W/"doc-v1", moet het ook 304 retourneren. Het voorbeeld parseert tags tussen aanhalingstekens in plaats van ze blind op komma's te splitsen, omdat een komma binnen een tag kan voorkomen.

Toon aan dat ETag voorrang heeft

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

Verwacht 200 OK met het document. De tag komt niet overeen, dus de latere datum mag geen 304 activeren. Dit bewust tegenstrijdige verzoek spoort een implementatie op die de twee controles ten onrechte combineert.

Simuleer het publiceren van de volgende revisie

Druk in het servervenster op Control-C en start vervolgens revisie 2. Laat de verzoek-URL ongewijzigd.

python3 server.py 2

Herhaal de twee eerdere verzoeken voor hernieuwde validatie met de validators van revisie 1. Beide moeten nu 200 retourneren, met de bijgewerkte zin waarin een driehoek wordt genoemd, ETag: "doc-v2" en de wijzigingsdatum van 2 januari. Als je een verzoek herhaalt met If-None-Match: "doc-v2", moet het 304 retourneren.

Deze resultaten vormen een acceptatiereeks: eerste download, validatie van ongewijzigde inhoud, tegenstrijdige validators, gewijzigd document en vervolgens validatie van de nieuwe ongewijzigde revisie. Het voorbeeld is getest met 24 lokale controles over beide revisies, waaronder zwakke tags, taglijsten, jokervergelijking, HEAD en ontbrekende paden.

Waarom browser- en Google-verzoeken er anders uit kunnen zien

Browsers verwerken HTTP-validators normaal gesproken via hun cache. Het WHATWG Fetch-algoritme voegt opgeslagen validators toe bij hernieuwde validatie en werkt na een geslaagde 304 de headers van de opgeslagen reactie bij en retourneert de opgeslagen reactie. Daardoor kan JavaScript van de toepassing een bruikbare reactie 200 en de documentbody ontvangen, ook al kreeg de netwerkvalidatie 304. Fetch Standard, HTTP-netwerk- of cacheverzoek.

Begin bij het onderzoeken van servergedrag met de expliciete curl-verzoeken hierboven. Als je in browser-JavaScript handmatig voorwaardelijke headers opgeeft, verandert de test: Fetch schakelt voor een verzoek met de standaardcache en die headers over op no-store. Laat de browser de cache beheren wanneer je normale browservalidatie test. Fetch Standard, cachemodus van verzoek.

De crawlinfrastructuur van Google ondersteunt beide validatorparen en beveelt ETags aan, omdat die problemen met datumopmaak vermijden. In de documentatie staat ook dat deze infrastructuur andere HTTP-cache-instructies niet ondersteunt. Kies het cachebeleid voor browsers los van je verwachtingen over het opnieuw crawlen door Google. Overzicht van Google-crawlers en fetchers.

Google stuurt niet bij elke crawlpoging voorwaardelijke headers mee. In de Search-documentatie staat dat een 304 hergebruik van eerder gecrawlde inhoud toestaat, terwijl nieuwere inhoud een 200 met de bijgewerkte body kan krijgen. Zorg dat gewone onvoorwaardelijke verzoeken blijven werken; een crawlerverzoek zonder validators bewijst niet dat je implementatie is mislukt. Problemen met crawl-fouten in Google Search oplossen.

Doorloop de acceptatiereeks met de daadwerkelijke URL waarop het document wordt aangeboden voordat je dit patroon op een gepubliceerd document toepast. Controleer of ongewijzigde revisies hun validators behouden, wijzigingen de validators bijwerken en een 304 geen documentbody bevat. Het gewenste resultaat is dat documenten correct worden hergebruikt en gewijzigde inhoud beschikbaar blijft.

Bronnen