Introducción
Un catálogo público de soporte recibe solicitudes repetidas para el mismo idioma y la misma categoría. Reutilizar las respuestas puede reducir el trabajo repetido, pero una caché nunca debe mezclar datos específicos de clientes ni convertir un error en contenido público almacenado. En este laboratorio observará la generación sin caché, agregará una política de caché explícita, probará la caducidad y la invalidación dirigida, y después implementará la solución para verificar sus límites.
Este laboratorio independiente comienza en /home/labex/project/public-cache con Node.js 22.22.0, Wrangler 4.131.1 instalado localmente en el proyecto, Miniflare 4.20260730.0 para la evaluación aislada y un accesorio de respuestas sintéticas. Use su propia cuenta de aprendizaje y los flujos de autorización, implementación y gestión de secretos enseñados anteriormente. No se necesita ninguna máquina virtual, recurso, dominio comprado, base de datos ni actualización de pago previa. Las solicitudes cuentan para el uso normal de la cuenta.
Mantenga abierto un terminal. Todos los datos del catálogo y las credenciales son sintéticos. El contenido de la Cache API es local a una ubicación de servicio; una red global no equivale a una caché replicada globalmente. Al finalizar, elimine el Worker, borre el secreto local y cierre la sesión.
Observar respuestas nuevas del catálogo público
En este paso inspeccionará un catálogo sintético proporcionado y comprobará su comportamiento sin caché. El accesorio genera un UUID nuevo para cada respuesta, de modo que pueda observar la reutilización sin depender de suposiciones sobre los tiempos ni de una base de datos.
cd /home/labex/project/public-cache
node --version
npx wrangler --version
cat src/catalog.js
Espere ver Node.js v22.22.0 y Wrangler 4.131.1. La configuración instaló las dependencias exactas del proyecto; para reproducir una instalación existente con su archivo de bloqueo, use npm ci. El entorno de evaluación también utiliza Miniflare 4.20260730.0, en consonancia con la fecha de compatibilidad. El accesorio varía según el idioma, la categoría y el cliente sintético; puede simular un error 503 con X-Demo-Failure: 1. Estos son datos de prueba, no credenciales de identidad reales.
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
Espere a que el servicio esté listo antes de enviar solicitudes. Si el inicio aún está en curso, vuelva a ejecutar cat dev.log. Mantenga este terminal abierto para conservar sus variables de shell.
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"
Ambas solicitudes devuelven 200, con audience igual a public y UUID distintos en generation. X-Lab-Cache: BYPASS indica que este controlador no consultó ni escribió en su caché. Cache-Control: no-store, visible para el cliente, mantiene fuera del experimento la caché del navegador o del cliente. Use la verificación antes de sustituir esta línea base.
Almacenar en caché solo las respuestas públicas aptas
En este paso agregará la consulta y el almacenamiento mediante la Cache API. Detenga el trabajo actual de desarrollo que aparece con jobs; en este ejemplo se supone que es el trabajo 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
La clave utiliza el origen actual junto con una ruta fija, la categoría y el idioma. El orden de los parámetros es canónico, mientras que las dos dimensiones del contenido permanecen diferenciadas. Los parámetros desconocidos y las dimensiones duplicadas se rechazan en lugar de cambiar silenciosamente el significado de la clave.
La elegibilidad se comprueba antes de consultar la caché. Las cabeceras Authorization, Cookie y de cliente sintético omiten una entrada pública ya disponible. El accesorio de errores también omite la consulta, para que un error no quede oculto por una respuesta correcta almacenada. Solo se almacena una respuesta correcta que no incluya Set-Cookie. Se crea una copia porque los cuerpos de las respuestas son streams, se asigna a la copia almacenada un TTL de 10 segundos y se espera a que termine la escritura. Las respuestas devueltas conservan no-store; la entrada interna de la Cache API tiene su propia política de caché.
npx wrangler dev --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &
cat dev.log
Espere a que el servicio esté listo antes de enviar solicitudes. Si el inicio aún está en curso, vuelva a ejecutar cat dev.log. Mantenga este terminal abierto para conservar sus variables de shell.
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"
Ejecute las dos primeras solicitudes en un intervalo inferior a diez segundos. La primera respuesta que no estaba en caché muestra MISS; una repetición muestra HIT y conserva el mismo valor de generation. Invertir el orden de los parámetros de consulta no cambia la clave. Las variantes francesa y de impresora contienen las dimensiones solicitadas y tienen entradas independientes. Si el TTL caduca mientras realiza las comprobaciones, repita rápidamente un par de solicitudes; no suponga que el contenido de la caché permanece para siempre.
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"
Las solicitudes que contienen datos de cliente o identidad devuelven BYPASS y la audiencia sintética correspondiente; nunca devuelven el resultado de otro cliente. El error simulado devuelve 503 BYPASS incluso cuando hay datos públicos disponibles en la caché. Una solicitud pública posterior sigue devolviendo datos públicos, no el error. Use la verificación con el servidor local en ejecución. Esta también ejecuta el controlador en un entorno local aislado; no modifica una caché en la nube.
Caducar e invalidar una entrada de caché local
En este paso agregará una operación de invalidación autenticada para la misma clave canónica. Esta eliminación se realiza en el centro de datos local, no mediante una purga global. Detenga el trabajo de desarrollo real antes de editar.
jobs
kill %1
umask 077
PURGE_TOKEN=$(openssl rand -hex 24)
printf 'PURGE_TOKEN=%s\n' "$PURGE_TOKEN" > .dev.vars
cat .gitignore
Mantenga el secreto sintético fuera de Git, de la configuración pública, de las URL y de los registros. Protege la operación DELETE de este laboratorio; no es un token de API de 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
DELETE valida la credencial antes de llamar a cache.delete con la misma clave GET utilizada para consultar y almacenar. El booleano devuelto indica si existía una entrada en esta ubicación. Una eliminación no autorizada debe dejar la entrada intacta. Las solicitudes desde otra ubicación todavía pueden encontrar su propia entrada.
npx wrangler dev --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &
cat dev.log
Espere a que el servicio esté listo antes de enviar solicitudes. Si el inicio aún está en curso, vuelva a ejecutar cat dev.log. Mantenga este terminal abierto para conservar sus variables de shell.
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"
Un DELETE no autorizado devuelve 401. Un DELETE válido devuelve scope: this-location y normalmente invalidated: true si la entrada aún está vigente. false también es un resultado válido si el TTL corto ya caducó. El siguiente GET devuelve MISS con una generación nueva. Para demostrar true, ejecute un GET inmediatamente antes del DELETE autorizado.
sleep 11
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
Después de once segundos, un nuevo MISS demuestra la caducidad sin una eliminación explícita. Use la verificación con el servidor en ejecución: un entorno aislado comprueba la reutilización, la separación de dimensiones, la exclusión de datos privados y errores, el rechazo de eliminaciones, una eliminación dirigida correcta, la conservación de otra clave y la caducidad. Estas comprobaciones locales controladas proporcionan pruebas repetibles sin suponer un estado global de la caché.
La documentación de Cache API explica su alcance por centro de datos, el comportamiento de las cabeceras de respuesta y cache.delete. Cache API y el almacenamiento en caché de la plataforma que omite la ejecución del Worker son mecanismos distintos.
Implementar y comprobar los límites de la caché
En este paso implementará el controlador completo en su cuenta de aprendizaje. Detenga el trabajo local real, autorice esta máquina virtual nueva y después compruebe la identidad de la cuenta.
jobs
kill %1
npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_tail:read
Complete en el navegador donde inició sesión el enlace y el código del dispositivo que se muestran, revise que los permisos y el acceso en segundo plano no hayan cambiado y seleccione su cuenta de aprendizaje. Espere a que el terminal confirme el éxito.
npx wrangler whoami --json
Confirme el nombre de la cuenta prevista. Sustituya YOUR_ACCOUNT_ID por su ID real y conserve el nombre único.
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
El archivo de secreto local no se carga mediante deploy; el comando bulk explícito crea PURGE_TOKEN como secret_text. Espere un breve intervalo para que el despliegue se propague. Copie a continuación la URL pública real que muestre 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"
Las respuestas públicas deben incluir el idioma y la categoría solicitados, así como la audiencia pública. Las repeticiones desde la misma ubicación dentro del TTL pueden mostrar HIT y conservar una generación; otra ubicación o la caducidad pueden producir legítimamente MISS. No afirme que el contenido está compartido globalmente basándose en dos solicitudes. Las solicitudes privadas siempre deben omitir la caché, y el error debe ser 503 BYPASS.
En la misma cuenta del Dashboard, abra Compute → Workers & Pages y confirme el Worker exacto y su URL de workers.dev. Use la verificación para comprobar la propiedad, la asociación del secreto implementado y los límites de las respuestas. No realiza ninguna invalidación en la nube. El comportamiento de invalidación se probó localmente; cache.delete no es un mecanismo de purga global. Si el despliegue aún se está propagando, espere brevemente y repita las comprobaciones de respuesta; investigue cualquier discrepancia persistente en lugar de aceptarla.
Eliminar el Worker desechable
En este paso eliminará el despliegue de este laboratorio mientras todavía tiene autorización. Confirme el nombre único y la cuenta, y después elimine únicamente este Worker.
cat wrangler.jsonc
npx wrangler delete
Cuando aparezca la solicitud con el nombre correspondiente, pulse la única tecla y. Wrangler 4.131.1 puede mostrar después de la eliminación el diagnóstico conocido de autenticación heredada de Workers Sites KV. No amplíe los permisos ni trate ese error como una prueba. Actualice el Dashboard y use la verificación: un inventario autenticado correctamente debe mostrar que este Worker exacto ya no existe. Conserve la cuenta de aprendizaje y su subdominio. Eliminar el Worker no significa que todas las entradas de caché se hayan purgado globalmente; las entradas sintéticas tienen un TTL de diez segundos y no debe quedar ninguna aplicación en ejecución.
Eliminar el secreto local y desconectar la máquina virtual
En este paso eliminará la credencial local desechable después de verificar la eliminación y, a continuación, desconectará esta máquina virtual.
rm .dev.vars
unset PURGE_TOKEN
npx wrangler logout
npx wrangler whoami --json
Exija explícitamente loggedIn: false; el comando estructurado sin autenticación puede terminar con un código distinto de cero. Use la verificación y finalice la máquina virtual. La sesión del navegador puede permanecer disponible. Cerrar la sesión o finalizar la máquina virtual no elimina por sí solo un despliegue en la nube.
Resumen
Sustituyó la generación del catálogo sin caché por un almacenamiento explícito de respuestas públicas, conservó la separación de claves por idioma y categoría y omitió las solicitudes privadas y fallidas antes de consultar la caché. Probó entradas de corta duración e invalidación autenticada en un entorno local controlado, y después verificó la identidad y los límites de respuesta de la aplicación implementada sin suponer que el contenido de la caché estuviera compartido globalmente.
El TTL de la copia almacenada y la política de caché del cliente tienen propósitos distintos. La elegibilidad deliberada, las claves completas y las generaciones de respuesta observables permiten comprobar esta diferencia. Eliminó el despliegue desechable y la credencial local antes de desconectar la máquina virtual.

