Introducción
Un visor de documentos a menudo solo necesita los siguientes bytes o una confirmación de que su copia en caché sigue actualizada. Descargar el archivo completo en cada solicitud desperdicia trabajo. Añadirá validadores HTTP y descargas de rangos de un solo byte a un Worker protegido respaldado por almacenamiento privado de R2.
Complete primero Stream Documents Through a Worker. Este laboratorio comienza en una VM nueva con Node.js 22.22.0, Wrangler 4.131.1 y un módulo proporcionado para comprobar tokens; usted creará un bucket nuevo y desplegará un Worker nuevo. Su suscripción de R2 y los permisos de su cuenta de aprendizaje deben estar preparados de antemano. Consulte precios de R2 para conocer las operaciones y el almacenamiento. No se necesita ningún dominio personalizado. Solo se almacenará texto sintético; haga la limpieza antes de salir.
Conectar el bucket de la aplicación
En este paso, autorizará esta VM y creará un bucket privado independiente para la aplicación. La autorización del dispositivo confirma su cuenta de aprendizaje. La administración de buckets de R2 utiliza un token de API independiente, restringido a esa cuenta.
Inicie Bash para usar la sintaxis de comandos que aparece a continuación. Después, vaya al proyecto preparado y compruebe sus herramientas. Mantenga abierta esta misma terminal para conservar disponibles las variables con los nombres de los recursos:
bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version
Autorice el código de dispositivo mostrado en su propio navegador. Confirme la cuenta de aprendizaje y los ámbitos de lectura de la cuenta y del usuario solicitados antes de conceder el consentimiento:
npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write
npx wrangler whoami --json
Exija loggedIn: true. Lea el nombre de la cuenta aunque solo aparezca una cuenta. Sustituya YOUR_ACCOUNT_ID en el comando siguiente por el ID real de 32 caracteres de esa cuenta. openssl rand -hex 6 genera doce caracteres hexadecimales aleatorios para evitar que este laboratorio entre en conflicto con una ejecución anterior. El documento here-document escribe un archivo de configuración estándar; el shell sustituye sus variables en él.
ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r03-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","main":"src/index.js","workers_dev":true,"compatibility_date":"2026-07-30","r2_buckets":[{"binding":"DOCUMENTS","bucket_name":"$BUCKET"}]}
JSON
Para administrar el bucket, abra la página API Tokens de su perfil de Cloudflare y cree un token personalizado con un nombre relacionado con este laboratorio. Conceda Account → Workers R2 Storage → Edit y restrinja Account Resources a la cuenta de aprendizaje cuyo ID guardó. Establezca una caducidad breve. No incluya otras cuentas ni permisos no relacionados. Este token de administración sirve para administrar buckets, incluida su creación y eliminación. En este laboratorio, el Worker accede a los objetos de R2 mediante su binding DOCUMENTS.
Copie el token una sola vez en este indicador oculto de la VM. umask 077 restringe el archivo a su usuario; read -s oculta la entrada. El archivo utiliza la variable de token estándar de Wrangler y queda excluido de Git.
umask 077
read -r -s -p 'R2 management API token: ' R2_MANAGEMENT_TOKEN; printf '\n'
printf 'CLOUDFLARE_API_TOKEN=%s\n' "$R2_MANAGEMENT_TOKEN" > .env.management
unset R2_MANAGEMENT_TOKEN
Use --env-file=.env.management únicamente para los comandos de administración de R2; whoami seguirá comprobando la autorización del dispositivo de la VM.
Coloque --env-file al final de cada comando de Wrangler para que su lista de archivos no incluya el nombre del comando. Después de crear cada bucket, si Wrangler pregunta si debe añadir un enlace a la configuración, escriba n y pulse Enter. La configuración ya contiene el enlace previsto.
npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management
Enumere sus buckets y busque el nombre generado exacto. Los demás buckets pertenecen a otros trabajos; no los modifique.
npx wrangler r2 bucket list --env-file=.env.management
En Dashboard, abra Storage & databases → R2 → Overview, seleccione este bucket exacto y compruebe que la lista de objetos esté vacía. En su configuración, mantenga desactivadas la URL pública de desarrollo y los dominios personalizados. El nombre del bucket en Dashboard confirma la identidad; las comprobaciones de descarga posteriores demostrarán cuáles son los bytes almacenados.
El permiso para scripts de Worker permite el despliegue. El permiso para KV permite que Wrangler registre la eliminación; este laboratorio no crea ningún espacio de nombres de KV. El token de administración de R2 sigue siendo una credencial independiente, limitada a la cuenta.
Implementar lecturas condicionales y parciales
En este paso, utilizará los metadatos de R2 para decidir si se necesita un cuerpo. Un ETag funciona como una etiqueta de versión del archivo. Cuando un cliente ya tiene una copia, envía la etiqueta en If-None-Match para preguntar si el archivo ha cambiado. Si coincide, se produce 304 Not Modified sin cuerpo, lo que evita volver a descargar los mismos bytes. Una solicitud Range permite que un visor descargue una parte de un archivo grande o reanude una descarga interrumpida. Solicita posiciones de bytes inclusivas y produce 206 Partial Content, incluido un encabezado Content-Range que describe el segmento.
Utilice este controlador. head() lee los metadatos sin leer bytes. La llamada posterior a get() incluye onlyIf.etagMatches, de modo que un objeto modificado entre ambas llamadas no se devuelva usando metadatos obsoletos. Este endpoint admite un único rango y If-Range basado en ETag; la sintaxis no compatible con varios rangos devuelve 400. Cuando un ETag de If-Range no coincide, una respuesta completa 200 permite que el cliente reemplace su copia anterior.
cat > src/index.js <<'JS'
import { authorized } from "./auth.js";
export default {
async fetch(request, env) {
const path = new URL(request.url).pathname;
if (path === "/health") return new Response("ok");
if (!await authorized(request, env)) return new Response("Unauthorized", { status: 401 });
if (request.method !== "GET") return new Response("Method not allowed", { status: 405 });
if (path !== "/documents/report.txt") return new Response("Not found", { status: 404 });
const key = path.slice(1);
const metadata = await env.DOCUMENTS.head(key);
if (!metadata) return new Response("Not found", { status: 404 });
const headers = new Headers({ "ETag": metadata.httpEtag,
"Last-Modified": metadata.uploaded.toUTCString(), "Accept-Ranges": "bytes",
"Cache-Control": "private, no-store" });
metadata.writeHttpMetadata(headers);
// GET validators use weak comparison: W/"value" and "value" can match.
const noneMatch = request.headers.get("If-None-Match");
if (noneMatch && noneMatch.split(",").some(tag => tag.trim() === "*" || tag.trim().replace(/^W\//, "") === metadata.httpEtag))
return new Response(null, { status: 304, headers });
const since = Date.parse(request.headers.get("If-Modified-Since") || "");
const uploadedSeconds = Math.floor(metadata.uploaded.getTime() / 1000) * 1000;
if (!noneMatch && Number.isFinite(since) && uploadedSeconds <= since)
return new Response(null, { status: 304, headers });
let range = request.headers.get("Range");
const ifRange = request.headers.get("If-Range");
if (ifRange && ifRange !== metadata.httpEtag) range = null;
let start = 0, end = metadata.size - 1;
if (range) {
const match = /^bytes=(\d*)-(\d*)$/.exec(range);
// This endpoint supports exactly one range, not multipart ranges.
if (!match || (!match[1] && !match[2]))
return new Response("Invalid range", { status: 400 });
if (!match[1]) { start = Math.max(0, metadata.size - Number(match[2])); }
else { start = Number(match[1]); if (match[2]) end = Math.min(Number(match[2]), end); }
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start > end || start >= metadata.size) {
headers.set("Content-Range", `bytes */${metadata.size}`);
return new Response("Range not satisfiable", { status: 416, headers });
}
headers.set("Content-Range", `bytes ${start}-${end}/${metadata.size}`);
}
// Do not mix a HEAD result with bytes from an object replaced in between.
const object = await env.DOCUMENTS.get(key, { onlyIf: { etagMatches: metadata.etag },
...(range ? { range: { offset: start, length: end - start + 1 } } : {}) });
if (!object) return new Response("Not found", { status: 404 });
if (!("body" in object)) return new Response("Object changed; retry", { status: 412 });
headers.set("Content-Length", String(range ? end - start + 1 : metadata.size));
return new Response(object.body, { status: range ? 206 : 200, headers });
}
};
JS
La posición inicial solicitada se basa en cero. Un sufijo como bytes=-3 significa los tres últimos bytes. Una posición inicial posterior al objeto produce 416 con Content-Range: bytes */SIZE. La validación condicional tiene prioridad sobre la selección del rango. If-None-Match tiene prioridad sobre la validación de fecha cuando ambos están presentes.
Cree el secreto local de la aplicación y compruebe el paquete:
umask 077
printf "ACCESS_TOKEN=%s\n" "$(openssl rand -hex 24)" > .dev.vars
npx wrangler deploy --dry-run
Comparar cuerpos locales completos y parciales
En este paso, preparará datos únicamente en el almacenamiento local y examinará encabezados HTTP reales. Un objeto local es independiente del objeto remoto posterior, aunque ambos utilicen la misma clave.
npx wrangler r2 object put "$BUCKET/documents/report.txt" --local --file document.txt --content-type text/plain
npx wrangler dev --ip 127.0.0.1 --port 8787 > dev.log 2>&1 &
DEV_PID=$!
Espere el mensaje de disponibilidad en dev.log y, después, cargue el secreto sintético de la aplicación:
cat dev.log
set -a
source .dev.vars
set +a
Guarde por separado los encabezados y el cuerpo de la respuesta completa. -D escribe los encabezados en un archivo:
curl -fsS -D full.headers -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/report.txt -o full.txt
cmp document.txt full.txt
cat full.headers
Exija 200, el tipo de contenido almacenado, un ETag entre comillas y Accept-Ranges: bytes. Copie el ETag exacto, incluidas las comillas dobles, en ETAG dentro de las comillas simples que se muestran a continuación:
ETAG='"COPY_ETAG_HERE"'
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "If-None-Match: $ETAG" http://127.0.0.1:8787/documents/report.txt
Exija 304 sin cuerpo. Un validador actualizado evita una transferencia completa; no hace público el bucket.
curl -sS -D range.headers -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=0-4" http://127.0.0.1:8787/documents/report.txt -o range.txt
head -c 5 document.txt > expected-range.txt
cmp expected-range.txt range.txt
cat range.headers
Exija 206, Content-Range: bytes 0-4/SIZE y exactamente cinco bytes coincidentes. Ahora solicite una posición inicial no satisfacible:
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=99999-" http://127.0.0.1:8787/documents/report.txt
Exija 416, el encabezado bytes */SIZE y Range not satisfiable. La comprobación de la plataforma repite estas lecturas de forma independiente.
Verificar la entrega condicional remota
En este paso, preparará el recurso remoto de prueba de forma independiente y publicará el controlador. Detenga el servidor local y, después, cargue el mismo archivo sintético usando la opción explícita --remote:
kill "$DEV_PID"
wait "$DEV_PID" 2>/dev/null || true
npx wrangler r2 object put "$BUCKET/documents/report.txt" --remote --file document.txt --content-type text/plain --env-file=.env.management
npx wrangler deploy
npx wrangler secret bulk .dev.vars
Copie la URL desplegada en BASE_URL. Espere a que el endpoint de estado devuelva ok; si el nuevo despliegue todavía se está propagando, repita las lecturas durante un máximo de un minuto.
BASE_URL=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
curl -i "$BASE_URL/health"
curl -fsS -D remote.headers -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/report.txt" -o remote.txt
cmp document.txt remote.txt
cat remote.headers
Use el ETag remoto de remote.headers, no un valor local recordado. Repita las solicitudes condicionales y parciales:
ETAG='"COPY_REMOTE_ETAG_HERE"'
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "If-None-Match: $ETAG" "$BASE_URL/documents/report.txt"
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=0-4" "$BASE_URL/documents/report.txt"
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" -H "Range: bytes=99999-" "$BASE_URL/documents/report.txt"
Exija 304 sin cuerpo, 206 con los cinco primeros bytes del recurso de prueba y 416 con el límite de tamaño correcto. En Dashboard, compruebe el binding exacto del Worker y el objeto del bucket. Mantenga desactivados la URL pública del bucket y los dominios personalizados; los encabezados HTTP y las comparaciones de cuerpos son la evidencia autorizada del rango.

Este ejemplo muestra DOCUMENTS conectado al bucket privado exacto. El sufijo del nombre generado será diferente.

La fila muestra report.txt como text/plain, Standard y 41 B, mientras Public Access sigue en Disabled. Los nombres generados y las fechas son ejemplos. El valor agregado Bucket Size puede seguir en 0 B por retraso; la fila del objeto y la comparación de bytes demuestran que el archivo existe. Las cabeceras HTTP y las comparaciones del cuerpo verifican el comportamiento condicional y por rangos.
Eliminar la aplicación y el bucket remotos
En este paso, eliminará únicamente el Worker y los objetos de este laboratorio mientras sigue autorizado. El bucket privado no desaparece cuando se elimina su Worker.
npx wrangler delete
Confirme el nombre exacto del Worker generado. Elimine explícitamente el único objeto cargado y, después, elimine el bucket:
BUCKET=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).r2_buckets[0].bucket_name")
npx wrangler r2 object delete "$BUCKET/documents/report.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
Solo se creó de forma remota documents/report.txt. Si existen otros objetos, inspeccione este bucket exacto y confirme quién es el propietario antes de eliminarlos.
Actualice las listas de Workers y buckets en Dashboard y ejecute la comprobación de limpieza de la plataforma. Los fallos de autenticación o de red no son concluyentes y no demuestran una eliminación correcta.
Revocar las credenciales restantes
En este paso, revocará el token de administración de este laboratorio en la página de tokens de API de su perfil, eliminará el secreto local de la aplicación y cerrará la autorización de la VM. Hágalo únicamente después de que la comprobación de limpieza anterior se complete correctamente.
rm .env.management .dev.vars
unset ACCESS_TOKEN
npx wrangler logout
npx wrangler whoami --json || true
Exija loggedIn: false. Revocar el token de administración es un paso manual independiente en Dashboard; eliminar solo el archivo local no lo revoca. Mantenga intactos el inicio de sesión habitual de Dashboard y los tokens de otros laboratorios.
Resumen
Utilice los metadatos de R2 para las respuestas condicionales, transmita rangos de un solo byte, gestione las solicitudes no satisfechas y limpie el servicio privado de descargas.



