Implemente GET condicional enviando validadores estáveis com cada documento, verificando os validadores salvos pelo cliente e retornando o documento atual ou uma resposta 304 Not Modified sem corpo. Este tutorial para Mac demonstra o processo com uma página de texto sintética.

A tarefa é fazer com que solicitações repetidas de um documento público transfiram seu conteúdo somente quando necessário. Você executará um pequeno servidor Python local, fará testes no Terminal e simulará uma atualização do documento. O exemplo permanece local; publicá-lo é uma tarefa separada.

Como o servidor deve escolher entre 200 e 304?

Uma solicitação inicial recebe 200 OK, o corpo do documento e seus validadores. Uma solicitação condicional posterior envia de volta os validadores salvos. Se o documento não mudou, o servidor retorna 304 Not Modified, permitindo que o cliente reutilize o corpo armazenado. Conteúdo alterado recebe uma nova resposta 200. Solicitações HTTP condicionais.

Para um documento existente que, de outro modo, retornaria 200, aplique esta sequência de decisões. If-None-Match tem precedência sobre If-Modified-Since; não são dois testes independentes. RFC 9110, campos de solicitação condicional.

Condição da solicitação Resposta
Sem cabeçalhos condicionais 200 com o documento
If-None-Match corresponde à tag atual 304, sem corpo
If-None-Match está presente, mas não corresponde 200, mesmo que uma data fornecida indique que não houve alteração
Apenas um If-Modified-Since válido é fornecido, e o documento não é mais recente 304, sem corpo
Apenas um If-Modified-Since válido é fornecido, e o documento é mais recente 200 com o documento

Avalie essas condições após selecionar o documento e estabelecer sua resposta normal. Um documento ausente ainda deve produzir a resposta normal de recurso inexistente. Uma 304 não pode conter conteúdo nem trailers. Ela deve incluir os metadados de cache aplicáveis, inclusive os campos Date, ETag, Vary, Cache-Control, Expires e Content-Location que acompanhariam sua resposta 200. RFC 9110, avaliação de pré-condições e requisitos para 304.

Escolha validadores que descrevam o documento publicado

Uma ETag é um identificador de versão entre aspas, como "doc-v1". O método de geração é definido pela implementação: um identificador de revisão ou um hash do conteúdo podem funcionar. Mantenha-a estável para a mesma representação e altere-a quando essa representação mudar. O exemplo usa duas revisões explícitas cujo conteúdo é fixo. Cabeçalho ETag.

Uma tag forte identifica uma representação específica; uma tag fraca tem o prefixo W/ e pode descrever conteúdo semanticamente equivalente cujos bytes sejam diferentes. As comparações condicionais GET com If-None-Match usam comparação fraca, então W/"doc-v1" corresponde a "doc-v1". As solicitações também podem conter uma lista de tags ou o curinga isolado *. RFC 9110, If-None-Match.

Last-Modified descreve quando o documento mudou. Use um horário de modificação determinado de forma consistente, incorporando os componentes relevantes do documento, em vez do horário de cada solicitação. Ele não pode ser posterior ao Date da resposta. RFC 9110, geração de Last-Modified.

O exemplo atribui horários de publicação deliberadamente ilustrativos às duas revisões sintéticas: meio-dia GMT de 1º e 2 de janeiro de 2024. Essas datas são dados de teste. Em um fluxo de publicação real, use metadados de publicação mantidos. Last-Modified é menos preciso que uma ETag para detectar diferenças no conteúdo e oferece uma alternativa útil. Cabeçalho Last-Modified.

Selecione uma política de cache separadamente

Os validadores indicam se uma cópia armazenada ainda corresponde ao original. A política de atualização determina quando o cache precisa consultar o servidor. Este tutorial envia Cache-Control: no-cache, que permite o armazenamento, mas exige validação bem-sucedida antes da reutilização. Assim, é fácil observar o comportamento de solicitações repetidas. no-store proíbe o armazenamento, enquanto max-age permite reutilizar uma resposta enquanto ela estiver atualizada. RFC 9111, diretivas de resposta para cache.

Para um documento público que possa tolerar um pequeno atraso até que uma atualização fique visível, uma política como max-age=60 pode ser adequada. Essa é uma escolha editorial ilustrativa: escolha o intervalo de acordo com os requisitos de atualização do documento. Uma resposta armazenada ainda atualizada pode ser reutilizada sem contatar o servidor, portanto, cada visita repetida não precisa gerar um 304. RFC 9111, atualização.

Execute um exemplo local completo no seu Mac

Abra o Terminal e confira os pré-requisitos com os comandos a seguir. Use o Python 3.9 ou mais recente porque o programa usa str.removeprefix. Se python3 não estiver disponível, instale o Python 3 antes de continuar. Não são necessários pacotes Python de terceiros.

python3 --version
curl --version

Em uma nova pasta de trabalho, salve o programa completo a seguir como server.py usando um editor de código de texto simples. Ele serve apenas /page.txt, implementa GET e HEAD, e seleciona uma revisão do documento com base no argumento da linha de comando. O BaseHTTPRequestHandler do Python encaminha solicitações para os métodos correspondentes e fornece funções auxiliares para escrever respostas. Documentação do servidor HTTP do Python.

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

Esta é uma demonstração local dos dois validadores mencionados, não uma pilha completa de atendimento HTTP. A conversão de datas usa o analisador permissivo de datas de e-mail do Python; uma implementação implantada deve usar validação de datas específica para HTTP e tratamento de solicitações condicionais. A documentação do Python descreve as entradas aceitas e as exceções desse analisador. Utilitários de data de e-mail do Python.

No Terminal, navegue até a pasta que contém server.py e inicie a revisão 1. Deixe essa janela aberta. Se a porta 8000 já estiver ocupada, escolha outra porta disponível no programa e use a mesma porta em todas as URLs de teste.

python3 server.py 1

Teste conteúdo inalterado, conteúdo alterado e precedência

Abra outra janela do Terminal. As solicitações a seguir exibem os cabeçalhos da resposta e qualquer corpo retornado. A opção -i inclui os cabeçalhos, -H fornece um cabeçalho de solicitação e --max-time limita a duração de cada transferência. Manual do curl.

Estabeleça a resposta inicial

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

Espere receber 200 OK, o texto da página sintética, ETag: "doc-v1" e Last-Modified: Mon, 01 Jan 2024 12:00:00 GMT. Ao aplicar este procedimento a outro documento, registre os valores realmente retornados; os validadores pertencem àquela resposta específica.

Revalide com cada validador

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

Ambas as solicitações devem retornar 304 Not Modified sem o texto da página. A resposta mantém ETag e Cache-Control; a função auxiliar de resposta do Python adiciona Date. Repetir a primeira solicitação com W/"doc-v1" também deve retornar 304. O exemplo analisa tags entre aspas em vez de simplesmente dividi-las por vírgulas, porque uma vírgula pode aparecer dentro de uma tag.

Comprove que ETag tem precedência

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

Espere receber 200 OK com o documento. A tag não corresponde, portanto, a data posterior não deve acionar um 304. Esta solicitação deliberadamente conflitante detecta uma implementação que combina os dois testes por engano.

Simule a publicação da próxima revisão

Na janela do servidor, pressione Control-C e, em seguida, inicie a revisão 2. Mantenha a URL da solicitação inalterada.

python3 server.py 2

Repita as duas solicitações de revalidação anteriores que contêm os validadores da revisão 1. Agora, ambas devem receber 200, a frase atualizada que menciona um triângulo, ETag: "doc-v2" e a data de modificação de 2 de janeiro. Repetir uma solicitação com If-None-Match: "doc-v2" deve retornar 304.

Esses resultados compõem uma sequência de aceitação: download inicial, validação sem alterações, validadores conflitantes, documento alterado e, em seguida, validação sem alterações da nova revisão. O exemplo foi exercitado com 24 verificações locais nas duas revisões, incluindo tags fracas, listas de tags, correspondência com curinga, HEAD e caminhos inexistentes.

Por que as solicitações do navegador e do Google podem parecer diferentes

Os navegadores normalmente gerenciam validadores HTTP por meio do cache. O algoritmo Fetch do WHATWG adiciona os validadores salvos ao revalidar e, após um 304 bem-sucedido, atualiza os cabeçalhos da resposta armazenada e retorna a resposta armazenada. Consequentemente, o JavaScript da aplicação pode receber uma resposta 200 utilizável e o corpo do documento, mesmo que a revalidação pela rede tenha recebido 304. Padrão Fetch, busca HTTP com rede ou cache.

Ao diagnosticar o comportamento do servidor, comece pelas solicitações explícitas do curl acima. Fornecer manualmente cabeçalhos condicionais no JavaScript do navegador altera o teste: o Fetch muda para no-store uma solicitação com cache padrão que contenha esses cabeçalhos. Deixe o navegador gerenciar o cache ao testar a revalidação normal no navegador. Padrão Fetch, modo de cache da solicitação.

A infraestrutura de rastreamento do Google é compatível com ambos os pares de validadores e recomenda ETags porque evitam problemas de formatação de datas. A documentação também informa que outras diretivas de cache HTTP não são compatíveis com essa infraestrutura. Escolha a política de cache do navegador separadamente das expectativas sobre o comportamento de novo rastreamento do Google. Visão geral dos rastreadores e buscadores do Google.

O Google não envia cabeçalhos condicionais em todas as tentativas de rastreamento. A documentação da Pesquisa explica que um 304 permite reutilizar conteúdo rastreado anteriormente, enquanto conteúdo mais recente pode receber um 200 com o corpo atualizado. Mantenha funcionando as solicitações comuns sem condições; uma solicitação de rastreador sem validadores não prova que sua implementação falhou. Solucionar erros de rastreamento da Pesquisa Google.

Antes de aplicar esse padrão a um documento publicado, repita a sequência de aceitação usando a URL real do documento. Confira se revisões inalteradas preservam seus validadores, se edições os atualizam e se uma 304 não contém o corpo do documento. O resultado desejado é a reutilização correta do documento, mantendo o conteúdo alterado acessível.

Fontes