Armazenar em cache respostas de APIs públicas

CloudflareBeginner
Pratique Agora

Introdução

Um catálogo público de suporte recebe solicitações repetidas para o mesmo idioma e a mesma categoria. Reutilizar respostas pode reduzir trabalho repetido, mas um cache nunca deve misturar dados específicos de clientes nem transformar um erro em conteúdo público armazenado. Você observará a geração sem cache, adicionará uma política explícita de cache, testará a expiração e a invalidação direcionada e, em seguida, fará o deploy para verificar esses limites.

Este laboratório independente começa em /home/labex/project/public-cache com Node.js 22.22.0, Wrangler 4.131.1 instalado localmente no projeto, Miniflare 4.20260730.0 para uma avaliação isolada e um fixture de resposta sintético. Use sua própria conta de aprendizado e os fluxos de autorização, deploy e gerenciamento de secrets ensinados anteriormente. Não é necessária uma VM anterior, um recurso, um domínio comprado, um banco de dados ou um upgrade pago. As solicitações contam para o uso normal da conta.

Mantenha um terminal aberto. Todos os dados do catálogo e as credenciais são sintéticos. O conteúdo da Cache API é local ao ponto de atendimento; uma rede global não é um cache globalmente replicado. Ao terminar, exclua o Worker, remova o secret local e saia da conta.

Observar respostas novas do catálogo público

Nesta etapa, você inspecionará um catálogo sintético fornecido e verificará seu comportamento sem cache. O fixture gera um UUID novo para cada resposta, tornando a reutilização observável sem depender de suposições sobre tempo ou de um banco de dados.

cd /home/labex/project/public-cache
node --version
npx wrangler --version
cat src/catalog.js

Espere obter Node.js v22.22.0 e Wrangler 4.131.1. A configuração instalou as dependências exatas do projeto; para reproduzir uma instalação existente usando o lockfile, use npm ci. O ambiente de avaliação também usa Miniflare 4.20260730.0, de acordo com a data de compatibilidade. O fixture varia conforme o idioma, a categoria e o cliente sintético; ele pode simular um erro 503 com X-Demo-Failure: 1. Essas são entradas de teste, não credenciais de identidade reais.

WORKER_NAME="labex-cache-$(openssl rand -hex 6)"
cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-07-30",
  "workers_dev": true,
  "preview_urls": false
}
CONFIG
cat > src/index.js <<'JS'
import {catalog} from './catalog.js';

function deliver(response, cacheStatus) {
  const headers = new Headers(response.headers);
  headers.set('X-Lab-Cache', cacheStatus);
  // This lab caches inside the Worker, not in the caller's browser.
  headers.set('Cache-Control', 'no-store');
  return new Response(response.body, {status: response.status, headers});
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === '/health' && request.method === 'GET') {
      return Response.json({status: 'ok'}, {headers: {'Cache-Control': 'no-store'}});
    }
    if (url.pathname !== '/api/catalog') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const language = url.searchParams.get('lang') || 'en';
    const category = url.searchParams.get('category') || 'network';
    if (!['en', 'fr'].includes(language) || !['network', 'printer'].includes(category) ||
        [...url.searchParams.keys()].some(key => !['lang', 'category'].includes(key)) ||
        url.searchParams.getAll('lang').length > 1 || url.searchParams.getAll('category').length > 1) {
      return Response.json({error: 'invalid_query'}, {status: 400, headers: {'Cache-Control': 'no-store'}});
    }
    if (request.method !== 'GET') {
      return Response.json({error: 'method_not_allowed'}, {status: 405, headers: {Allow: 'GET'}});
    }
    return deliver(catalog(request, language, category), 'BYPASS');
  }
};
JS
npx wrangler dev --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &
cat dev.log

Aguarde o servidor ficar pronto antes de enviar solicitações. Se a inicialização ainda estiver em andamento, execute cat dev.log novamente. Mantenha este terminal aberto para que as variáveis do shell continuem disponíveis.

curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"

As duas solicitações retornam 200, com audience igual a public e UUIDs generation diferentes. X-Lab-Cache: BYPASS significa que este handler não consultou nem gravou o cache. O Cache-Control: no-store voltado ao cliente mantém o cache do navegador/cliente fora do experimento. Use a verificação antes de substituir esse comportamento inicial.

Armazenar em cache somente respostas públicas elegíveis

Nesta etapa, você adicionará a consulta e o armazenamento usando a Cache API. Pare o processo atual de desenvolvimento identificado por jobs; o exemplo pressupõe que ele seja o job 1.

jobs
kill %1
cat > src/index.js <<'JS'
import {catalog} from './catalog.js';

function deliver(response, cacheStatus) {
  const headers = new Headers(response.headers);
  headers.set('X-Lab-Cache', cacheStatus);
  // This lab caches inside the Worker, not in the caller's browser.
  headers.set('Cache-Control', 'no-store');
  return new Response(response.body, {status: response.status, headers});
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === '/health' && request.method === 'GET') {
      return Response.json({status: 'ok'}, {headers: {'Cache-Control': 'no-store'}});
    }
    if (url.pathname !== '/api/catalog') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const language = url.searchParams.get('lang') || 'en';
    const category = url.searchParams.get('category') || 'network';
    if (!['en', 'fr'].includes(language) || !['network', 'printer'].includes(category) ||
        [...url.searchParams.keys()].some(key => !['lang', 'category'].includes(key)) ||
        url.searchParams.getAll('lang').length > 1 || url.searchParams.getAll('category').length > 1) {
      return Response.json({error: 'invalid_query'}, {status: 400, headers: {'Cache-Control': 'no-store'}});
    }
    const keyUrl = new URL('/api/catalog', url.origin);
    keyUrl.searchParams.set('category', category);
    keyUrl.searchParams.set('lang', language);
    const key = new Request(keyUrl, {method: 'GET'});
    const cache = caches.default;
    if (request.method !== 'GET') {
      return Response.json({error: 'method_not_allowed'}, {status: 405, headers: {Allow: 'GET'}});
    }
    // Decide eligibility before lookup: a warm public entry must not mask private work or errors.
    const bypass = ['Authorization', 'Cookie', 'X-Demo-Customer', 'X-Demo-Failure']
      .some(name => request.headers.has(name));
    if (bypass) return deliver(catalog(request, language, category), 'BYPASS');
    const cached = await cache.match(key);
    if (cached) return deliver(cached, 'HIT');
    const response = catalog(request, language, category);
    if (response.status !== 200 || response.headers.has('Set-Cookie')) {
      return deliver(response, 'BYPASS');
    }
    const stored = response.clone();
    stored.headers.set('Cache-Control', 'public, max-age=10');
    // Await completion here so the next request can observe the write.
    await cache.put(key, stored);
    return deliver(response, 'MISS');
  }
};
JS

A chave usa a origem atual, uma rota fixa, a categoria e o idioma. A ordem dos parâmetros é canônica, enquanto as duas dimensões de conteúdo continuam distintas. Parâmetros desconhecidos e dimensões duplicadas são rejeitados, em vez de alterarem silenciosamente o significado da chave.

A elegibilidade é verificada antes da consulta. Cabeçalhos Authorization, Cookie e de cliente sintético ignoram uma entrada pública já existente. O fixture de falha também ignora a consulta, para que um erro não seja ocultado por um sucesso armazenado. Somente uma resposta bem-sucedida sem Set-Cookie é armazenada. Nós a clonamos porque os corpos das respostas são streams, atribuímos à cópia armazenada um TTL de 10 segundos e aguardamos a gravação. As respostas retornadas continuam com no-store; a entrada interna da Cache API tem sua própria política de cache.

npx wrangler dev --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &
cat dev.log

Aguarde o servidor ficar pronto antes de enviar solicitações. Se a inicialização ainda estiver em andamento, execute cat dev.log novamente. Mantenha este terminal aberto para que as variáveis do shell continuem disponíveis.

curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
curl -i "http://127.0.0.1:8080/api/catalog?category=network&lang=en"
curl -i "http://127.0.0.1:8080/api/catalog?lang=fr&category=network"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=printer"

Execute as duas primeiras solicitações dentro de dez segundos. A primeira resposta sem cache informa MISS; uma repetição informa HIT e mantém o mesmo valor de generation. Inverter a ordem dos parâmetros da consulta não altera a chave. As variantes em francês e de impressora contêm as dimensões solicitadas e possuem entradas independentes. Se o TTL expirar enquanto você estiver lendo, repita um par de solicitações rapidamente; não presuma que o conteúdo do cache permaneça disponível para sempre.

curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "X-Demo-Customer: alice"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "X-Demo-Customer: bob"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "Authorization: Bearer synthetic"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "Cookie: demo=synthetic"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "X-Demo-Failure: 1"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"

As solicitações que contêm cliente/identidade retornam BYPASS e o público sintético correspondente, nunca o resultado de outro cliente. O erro simulado retorna 503 BYPASS mesmo quando há dados públicos aquecidos no cache. Uma solicitação pública posterior continua retornando dados públicos, não o erro. Use a verificação com o servidor local em execução. Ela também executa o handler em um ambiente local isolado; não modifica um cache na nuvem.

Expirar e invalidar uma entrada do cache local

Nesta etapa, você adicionará uma operação autenticada de invalidação para a mesma chave canônica. Essa é uma exclusão no data center local, não uma limpeza global. Pare o processo de desenvolvimento real antes de editar o arquivo.

jobs
kill %1
umask 077
PURGE_TOKEN=$(openssl rand -hex 24)
printf 'PURGE_TOKEN=%s\n' "$PURGE_TOKEN" > .dev.vars
cat .gitignore

Mantenha o secret sintético fora do Git, da configuração pública, das URLs e dos logs. Ele protege a operação DELETE deste laboratório; não é um token da API da Cloudflare.

cat > src/index.js <<'JS'
import {catalog} from './catalog.js';

function deliver(response, cacheStatus) {
  const headers = new Headers(response.headers);
  headers.set('X-Lab-Cache', cacheStatus);
  // This lab caches inside the Worker, not in the caller's browser.
  headers.set('Cache-Control', 'no-store');
  return new Response(response.body, {status: response.status, headers});
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === '/health' && request.method === 'GET') {
      return Response.json({status: 'ok'}, {headers: {'Cache-Control': 'no-store'}});
    }
    if (url.pathname !== '/api/catalog') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const language = url.searchParams.get('lang') || 'en';
    const category = url.searchParams.get('category') || 'network';
    if (!['en', 'fr'].includes(language) || !['network', 'printer'].includes(category) ||
        [...url.searchParams.keys()].some(key => !['lang', 'category'].includes(key)) ||
        url.searchParams.getAll('lang').length > 1 || url.searchParams.getAll('category').length > 1) {
      return Response.json({error: 'invalid_query'}, {status: 400, headers: {'Cache-Control': 'no-store'}});
    }
    const keyUrl = new URL('/api/catalog', url.origin);
    keyUrl.searchParams.set('category', category);
    keyUrl.searchParams.set('lang', language);
    const key = new Request(keyUrl, {method: 'GET'});
    const cache = caches.default;
    if (request.method === 'DELETE') {
      if (!env.PURGE_TOKEN) return Response.json({error: 'purge_unconfigured'}, {status: 503});
      if (request.headers.get('Authorization') !== `Bearer ${env.PURGE_TOKEN}`) {
        return Response.json({error: 'unauthorized'}, {status: 401, headers: {'Cache-Control': 'no-store'}});
      }
      const invalidated = await cache.delete(key);
      return Response.json({invalidated, scope: 'this-location'}, {
        headers: {'Cache-Control': 'no-store', 'X-Lab-Cache': 'BYPASS'}
      });
    }
    if (request.method !== 'GET') {
      return Response.json({error: 'method_not_allowed'}, {status: 405, headers: {Allow: 'GET, DELETE'}});
    }
    // Decide eligibility before lookup: a warm public entry must not mask private work or errors.
    const bypass = ['Authorization', 'Cookie', 'X-Demo-Customer', 'X-Demo-Failure']
      .some(name => request.headers.has(name));
    if (bypass) return deliver(catalog(request, language, category), 'BYPASS');
    const cached = await cache.match(key);
    if (cached) return deliver(cached, 'HIT');
    const response = catalog(request, language, category);
    if (response.status !== 200 || response.headers.has('Set-Cookie')) {
      return deliver(response, 'BYPASS');
    }
    const stored = response.clone();
    stored.headers.set('Cache-Control', 'public, max-age=10');
    // Await completion here so the next request can observe the write.
    await cache.put(key, stored);
    return deliver(response, 'MISS');
  }
};
JS

O DELETE valida a credencial antes de chamar cache.delete com a mesma chave GET usada na consulta e no armazenamento. O booleano retornado informa se havia uma entrada neste local. Uma exclusão não autorizada deve deixá-la intacta. Solicitações para outro local ainda podem encontrar a própria entrada.

npx wrangler dev --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &
cat dev.log

Aguarde o servidor ficar pronto antes de enviar solicitações. Se a inicialização ainda estiver em andamento, execute cat dev.log novamente. Mantenha este terminal aberto para que as variáveis do shell continuem disponíveis.

curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
curl -i -X DELETE "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
curl -i -X DELETE "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "Authorization: Bearer $PURGE_TOKEN"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"

Um DELETE não autorizado retorna 401. Um DELETE válido retorna scope: this-location e, normalmente, invalidated: true quando ainda existe uma entrada válida. O valor false também é significativo se o TTL curto já tiver expirado. O próximo GET retorna MISS com uma geração nova. Para demonstrar o valor true, execute um GET imediatamente antes do DELETE autorizado.

sleep 11
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"

Após onze segundos, um novo MISS demonstra a expiração sem uma exclusão explícita. Use a verificação com o servidor em execução: um ambiente isolado verifica a reutilização, a separação das dimensões, a exclusão de dados privados e erros, a rejeição da exclusão, a exclusão direcionada bem-sucedida, a preservação de uma chave diferente e a expiração. Essas verificações locais controladas fornecem evidências reproduzíveis sem presumir o estado de um cache global.

A documentação da Cache API explica seu escopo por data center, o comportamento dos cabeçalhos de resposta e cache.delete. A Cache API e o cache da plataforma que ignora a execução do Worker são mecanismos separados.

Fazer o deploy e verificar os limites do cache

Nesta etapa, você fará o deploy do handler concluído na sua conta de aprendizado. Pare o processo local real, autorize esta VM nova e depois verifique a identidade da conta.

jobs
kill %1
npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_tail:read

Conclua no navegador conectado à sua conta o link/código de dispositivo exibido, revise as permissões inalteradas e o acesso em segundo plano e selecione sua conta de aprendizado. Aguarde a confirmação de sucesso no terminal.

npx wrangler whoami --json

Confirme o nome da conta pretendida. Substitua YOUR_ACCOUNT_ID pelo ID real da conta abaixo, mantendo o nome exclusivo.

cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-07-30",
  "workers_dev": true,
  "preview_urls": false,
  "account_id": "YOUR_ACCOUNT_ID"
}
CONFIG
npx wrangler deploy
npx wrangler secret bulk .dev.vars
npx wrangler secret list

O arquivo de secret local não é enviado pelo deploy; o comando explícito bulk cria PURGE_TOKEN como secret_text. Aguarde um curto intervalo de propagação após o deploy. Copie abaixo a URL pública real exibida pelo Wrangler.

APP_URL="https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev"
curl -i "$APP_URL/api/catalog?lang=en&category=network"
curl -i "$APP_URL/api/catalog?category=network&lang=en"
curl -i "$APP_URL/api/catalog?lang=fr&category=network"
curl -i "$APP_URL/api/catalog?lang=en&category=network" -H "X-Demo-Customer: alice"
curl -i "$APP_URL/api/catalog?lang=en&category=network" -H "X-Demo-Customer: bob"
curl -i "$APP_URL/api/catalog?lang=en&category=network" -H "X-Demo-Failure: 1"

As respostas públicas devem conter o idioma e a categoria solicitados, além de audience igual a public. Repetições no mesmo local dentro do TTL podem mostrar HIT e manter a mesma geração; outro local ou a expiração pode produzir legitimamente MISS. Não conclua que o conteúdo é compartilhado globalmente com base em duas solicitações. As solicitações privadas sempre devem ignorar o cache, e a falha deve retornar 503 BYPASS.

Na mesma conta do Dashboard, abra Compute → Workers & Pages e confirme o Worker exato e sua URL workers.dev. Use a verificação para conferir a propriedade, o vínculo do secret implantado e os limites das respostas. Ela não executa nenhuma invalidação na nuvem. O comportamento de invalidação foi testado localmente; cache.delete não é um mecanismo de limpeza global. Se o deploy ainda estiver sendo propagado, aguarde brevemente e repita as verificações das respostas; investigue uma divergência persistente em vez de aceitá-la.

Excluir o Worker descartável

Nesta etapa, remova o deploy deste laboratório enquanto você ainda está autorizado. Confirme o nome exclusivo e a conta e, em seguida, exclua somente este Worker.

cat wrangler.jsonc
npx wrangler delete

Quando aparecer o prompt com o nome correspondente, pressione a tecla y uma única vez. O Wrangler 4.131.1 pode informar, após a exclusão, o diagnóstico conhecido de autenticação legada do Workers Sites KV. Não amplie as permissões nem trate esse erro como prova. Atualize o Dashboard e use a verificação: um inventário autenticado bem-sucedido deve mostrar que este Worker exato está ausente. Preserve a conta de aprendizado e seu subdomínio. Excluir o Worker não significa que todas as entradas de cache foram limpas globalmente; as entradas sintéticas têm TTL de dez segundos e nenhuma aplicação em execução deve permanecer.

Remover o secret local e desconectar

Nesta etapa, remova a credencial local descartável depois de verificar a exclusão e, em seguida, desconecte esta VM.

rm .dev.vars
unset PURGE_TOKEN
npx wrangler logout
npx wrangler whoami --json

Exija explicitamente loggedIn: false; o comando estruturado sem autenticação pode terminar com código diferente de zero. Use a verificação e encerre a VM. O login no navegador pode continuar disponível. Sair da conta ou encerrar a VM não exclui automaticamente um deploy na nuvem.

Resumo

Você substituiu a geração sem cache do catálogo por um cache explícito de respostas públicas, preservou a separação das chaves por idioma e categoria e ignorou solicitações privadas e com falha antes da consulta. Você testou entradas de curta duração e a invalidação autenticada em um ambiente local controlado e, em seguida, verificou a identidade e os limites das respostas da aplicação implantada sem presumir que o conteúdo do cache fosse compartilhado globalmente.

O TTL da cópia armazenada e a política de cache do cliente têm finalidades diferentes. A elegibilidade deliberada, as chaves completas e as gerações observáveis das respostas tornam essa distinção testável. Você removeu o deploy descartável e a credencial local antes de desconectar a VM.