Construir respuestas: new Response, status, headers, CORS y Response.json
Un Worker contesta construyendo una Response, nunca mutando la petición. Dominar su segundo argumento —status y headers—, el atajo Response.json, el papel del Content-Type y las cabeceras de CORS es controlar el sobre entero del mensaje, no solo su contenido.
En un Worker no se responde modificando lo que llega: se construye una Response nueva y se devuelve. Esa respuesta es un sobre con tres partes —un status, unas cabeceras y un cuerpo—, y casi todo el significado de una contestación HTTP vive en las dos primeras, no en la tercera. El Content-Type le dice al cliente cómo leer los bytes; las cabeceras de CORS le dicen al navegador si le está permitido leerlos siquiera. Controlar la Response es controlar el sobre entero, y de eso trata esta lección.
- Construir una
Responseconnew Response(body, init)y controlarstatusyheaders. - Usar
Response.json()como atajo que serializa y fija elContent-Typepor ti. - Fijar el
Content-Typecorrecto y entender por qué el cliente se guía por la cabecera. - Habilitar CORS con las cabeceras
Access-Control-Allowy responder al preflightOPTIONS.
El objeto Response y su init
El constructor new Response(body, init) toma un cuerpo y un objeto de inicialización. El cuerpo puede ser una cadena, un ArrayBuffer, un ReadableStream, un FormData o null para respuestas sin contenido. El init es donde fijas lo que de verdad dirige el intercambio:
status
El código HTTP: 200, 201, 404, 500. Por defecto 200. Acompáñalo de statusText si quieres un texto asociado.
headers
Los metadatos: Content-Type, Cache-Control, CORS, cookies. Un objeto plano, o un Headers cuando hay varias entradas.
body
El contenido: una cadena, un ArrayBuffer, un ReadableStream, un FormData o null para un 204 sin cuerpo.
Para casos sencillos basta un objeto literal de cabeceras. Cuando necesitas varias entradas del mismo nombre —varias cookies, por ejemplo— o construir las cabeceras poco a poco, usa la clase Headers, que expone set, append, get y delete:
const headers = new Headers();
headers.set("Content-Type", "text/html; charset=utf-8");
headers.append("Set-Cookie", "sesion=abc; HttpOnly; Secure");
headers.set("Cache-Control", "public, max-age=3600");
return new Response(cuerpoHtml, { status: 200, headers });
Hay un detalle que atrapa a todos: las cabeceras de una Response que recibes de fetch son inmutables. Si proxias una respuesta de un origen y quieres tocar una cabecera, no la mutes; construye una nueva a partir de ella, que sí es mutable:
const original = await fetch("https://origen.example.com/pagina");
const respuesta = new Response(original.body, original);
respuesta.headers.set("X-Servido-Por", "mi-worker");
return respuesta;
Response.json y el atajo de la cabecera
Devolver JSON es tan común que el estándar añadió un atajo estático. Response.json(dato, init) serializa el dato con JSON.stringify y fija por ti el Content-Type a application/json. Compara las dos formas:
// A mano: serializas y declaras el tipo tu mismo.
return new Response(JSON.stringify({ ok: true }), {
status: 201,
headers: { "Content-Type": "application/json" },
});
// Con el atajo: el mismo resultado, sin olvidos.
return Response.json({ ok: true }, { status: 201 });
El valor del atajo no es escribir menos, sino no olvidar el Content-Type. Un JSON servido sin esa cabecera es una fuente inagotable de bugs sutiles: el cliente recibe bytes correctos pero no sabe que son JSON, y response.json() en el otro extremo puede negarse a parsearlos. El atajo cierra esa puerta.
Content-Type: el cliente lee la cabecera
Aquí está la idea que reordena todo: el cliente decide qué es una respuesta por su Content-Type, no por su contenido. Los mismos bytes <h1>Hola</h1> servidos como text/plain se muestran como texto literal, con las etiquetas visibles; servidos como text/html los interpreta el navegador y renderiza un encabezado. El cuerpo no ha cambiado; ha cambiado la instrucción de cómo leerlo.
const bytes = "<h1>Hola</h1>";
// Se ve el texto crudo, etiquetas incluidas:
new Response(bytes, { headers: { "Content-Type": "text/plain" } });
// El navegador lo renderiza como HTML:
new Response(bytes, { headers: { "Content-Type": "text/html; charset=utf-8" } });
Por eso el charset importa —utf-8 evita que los acentos se rompan— y por eso conviene la cabecera X-Content-Type-Options con valor nosniff, que le prohíbe al navegador adivinar el tipo por su cuenta cuando la cabecera falta o es dudosa. El tipo de medio es una promesa que le haces al cliente sobre cómo tratar los bytes; incumplirla es la raíz de bugs de renderizado y de agujeros de seguridad.
CORS: por qué el navegador exige permiso
Un navegador impide por defecto que el JavaScript de un origen lea la respuesta de otro origen distinto: es la política del mismo origen, una barrera de seguridad. Para que tu API pueda consumirse desde otro dominio, tu Response debe llevar las cabeceras que conceden ese permiso, empezando por Access-Control-Allow-Origin. Y para métodos o cabeceras no triviales, el navegador envía antes una petición OPTIONS de sondeo —el preflight— que también debes contestar.
const cors = {
"Access-Control-Allow-Origin": "https://miapp.com",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
export default {
async fetch(request: Request): Promise<Response> {
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: cors });
}
return Response.json({ ok: true }, { headers: cors });
},
};
flowchart LR N[Navegador] -->|1 OPTIONS preflight| W[Worker] W -->|2 204 con cabeceras CORS| N N -->|3 POST real| W W -->|4 200 con Allow-Origin| N
Es tentador poner Access-Control-Allow-Origin a * y olvidarse. Sirve para APIs públicas de solo lectura, pero en cuanto la petición lleva credenciales —cookies o cabecera Authorization— el navegador rechaza el comodín: exige un origen concreto y, además, Access-Control-Allow-Credentials en true. Refleja el origen permitido desde una lista blanca en vez de abrir a todo el mundo.
Es fácil creer que el contenido de una respuesta es el cuerpo, y que las cabeceras son un envoltorio menor. La verdad es la contraria: los bytes del cuerpo son inertes, y casi todo el significado de una contestación HTTP vive en su metadato. El mismo cuerpo, byte por byte, es una página web o texto plano según su Content-Type; es cacheable durante una hora o nunca según su Cache-Control; es legible desde otro dominio o prohibido según sus cabeceras de CORS; existe como recurso nuevo o como error según su status. HTTP no es un protocolo de transporte de datos, es un protocolo de interpretación: el cuerpo dice qué, y las cabeceras dicen cómo, cuánto tiempo, para quién y con qué autoridad. El caso de CORS lo lleva al extremo y revela algo hermoso del modelo: la seguridad del mismo origen no la impone el servidor, la impone el navegador del cliente, y tu servidor no la vence, sino que declara un permiso que el navegador decide respetar. Tu Response no fuerza nada; ofrece un contrato —quién puede leerme, cómo debes tratarme, cuánto puedes guardarme— y confía en que el otro extremo lo honre. Por eso construir respuestas es la mitad seria del oficio en el edge: muchos frameworks te esconden las cabeceras tras helpers cómodos, y está bien hasta que algo se rompe y descubres que nunca controlaste el sobre, solo la carta. Un Worker te devuelve ese control crudo. Cuando internalizas que dominas el sobre entero —status, tipo, caché, permisos— y no solo el contenido, dejas de pelearte con comportamientos inexplicables del cliente y empiezas a diseñar el contrato exacto que quieres que el otro extremo obedezca.
- Construye un
404con cuerpotext/plainy, aparte, una variante con cuerpo JSON de error usandoResponse.json(). - Sirve la misma cadena
<h1>Hola</h1>comotext/plainy comotext/html, y observa en el navegador la diferencia. - Devuelve un JSON con
Response.json()y otro a mano conJSON.stringify, e inspecciona elContent-Typeque produce cada uno. - Añade CORS a un endpoint y contesta el preflight
OPTIONScon un204; pruébalo desde una página de otro origen. - Toma una respuesta de
fetchy cámbiale una cabecera construyendo unaResponsenueva a partir de ella.