wandres.dev
REQUEST Y RESPONSE · streaming en el edge

Leer el cuerpo de la petición: json, text, formData y arrayBuffer

El cuerpo de una Request es un flujo que se consume una sola vez, y el runtime ofrece cuatro lectores tipados para materializarlo: json, text, formData y arrayBuffer. Elegir el correcto no es cosmético: depende del método y del Content-Type, y equivocarse cuesta una excepción o un cuerpo ya agotado.

⏱ 15 min

Toda la información que un cliente te envía —el JSON de un POST, los campos de un formulario, los bytes de un fichero subido— viaja en el cuerpo de la Request. Y ese cuerpo no es una cadena que esté ahí esperándote: es un ReadableStream que llega por la red, se consume una única vez y que tú decides cómo materializar. El runtime te ofrece cuatro lectores tipados —json, text, formData y arrayBuffer— y toda la destreza de esta lección consiste en elegir el correcto guiándote por el método y por el Content-Type, no por la costumbre.

🎯 Al terminar esta lección sabrás
  • Entender que el cuerpo de una Request es un flujo que se consume una sola vez.
  • Conocer los cuatro lectores —json, text, formData, arrayBuffer— y cuándo usar cada uno.
  • Leer request.method y request.headers para decidir cómo interpretar el cuerpo.
  • Manejar los errores de parseo y clonar la petición cuando necesites leerla dos veces.

Un cuerpo que se consume una sola vez

Cuando tu fetch handler recibe una Request, su cuerpo todavía no está en memoria. Lo que tienes es un ReadableStream conectado a la conexión del cliente: los bytes van llegando y solo se materializan cuando tú los pides. Esta pereza es deliberada —te permite empezar a decidir antes de tener el cuerpo entero, o rechazar la petición sin llegar a leerlo—, pero impone una regla férrea del estándar Fetch: un cuerpo se lee una sola vez.

Cada uno de los cuatro lectores agota ese flujo. En cuanto invocas uno, la propiedad request.bodyUsed pasa a true y cualquier segunda lectura lanza una excepción, porque ya no queda nada que leer. No es un búfer que puedas releer a voluntad: es una cinta que se reproduce una vez y se consume al pasar.

export default {
  async fetch(request: Request): Promise<Response> {
    const texto = await request.text();
    // El flujo ya se agoto: la siguiente linea lanza una excepcion.
    const datos = await request.json();
    return new Response(texto);
  },
};
📝
Clona antes de leer dos veces

Si de verdad necesitas el mismo cuerpo en dos formatos —por ejemplo, verificar una firma HMAC sobre los bytes crudos y luego parsear el JSON—, clona la petición con request.clone() antes de consumir ninguna de las copias. El clon duplica el flujo para que cada rama lo lea entero por su cuenta. Úsalo con cabeza: mantener dos copias vivas obliga al runtime a bufferizar la distancia que una lleva sobre la otra.

Los cuatro lectores tipados

Los cuatro métodos leen el mismo flujo, pero cada uno lo interpreta de una forma y devuelve un tipo distinto. Elegir es, en el fondo, declarar qué esperas recibir:

🧩

request.json()

Parsea el cuerpo como JSON y devuelve el objeto ya deserializado. Para APIs con Content-Type application/json. Lanza si el cuerpo no es JSON válido.

📝

request.text()

Devuelve el cuerpo como una cadena UTF-8. Ideal para texto plano, XML o para inspeccionar el crudo antes de decidir qué hacer.

📋

request.formData()

Interpreta formularios application/x-www-form-urlencoded o multipart/form-data, incluidos los ficheros subidos, y te da un FormData.

💾

request.arrayBuffer()

Entrega los bytes crudos en un ArrayBuffer. Para datos binarios: imágenes, firmas, protocolos que no son texto.

Hay una jerarquía natural entre ellos. arrayBuffer es el más primitivo: los bytes tal cual. text es un arrayBuffer decodificado como UTF-8. json es un text pasado por JSON.parse. Y formData entiende la gramática de los formularios web, la única que además sabe separar varios ficheros y campos de un cuerpo multipart. Si pides un formato que el cuerpo no cumple —json sobre algo que no lo es—, la promesa se rechaza, así que envuelve el parseo en un try/catch cuando la entrada no sea de fiar.

El formData merece un vistazo porque es el más rico: un formulario puede mezclar campos de texto y ficheros en un mismo cuerpo multipart, y el FormData que obtienes los distingue por ti. Un campo de texto sale como cadena; un fichero subido sale como un objeto File del que puedes leer los bytes:

const form = await request.formData();
const nombre = form.get("nombre");        // un campo de texto
const avatar = form.get("avatar");        // un File si se subio uno
if (avatar instanceof File) {
  const bytes = await avatar.arrayBuffer();
  // ...por ejemplo, guardar esos bytes en R2
}

El método y las cabeceras mandan

Antes de leer, mira. El request.method te dice si siquiera cabe esperar un cuerpo: un GET o un HEAD no lo llevan, y las cabeceras Content-Type y Content-Length describen qué hay dentro y cuánto. Decidir el lector a partir de esa cabecera —y no de una suposición— es lo que separa un endpoint robusto de otro que se rompe con la primera petición inesperada.

export default {
  async fetch(request: Request): Promise<Response> {
    if (request.method !== "POST") {
      return new Response("metodo no permitido", { status: 405 });
    }
    const tipo = request.headers.get("Content-Type") ?? "";
    if (tipo.includes("application/json")) {
      const datos = await request.json();
      return Response.json({ recibido: datos });
    }
    if (tipo.includes("form")) {
      const form = await request.formData();
      return new Response(`campos: ${[...form.keys()].join(", ")}`);
    }
    return new Response("tipo no soportado", { status: 415 });
  },
};
flowchart TD
Req[Request entrante] --> M[revisar metodo]
M -->|GET o HEAD| Sin[sin cuerpo que leer]
M -->|POST PUT PATCH| CT[revisar Content-Type]
CT -->|application json| J[request.json]
CT -->|form o multipart| F[request.formData]
CT -->|texto plano| T[request.text]
CT -->|binario| A[request.arrayBuffer]

Fíjate en que cada rama devuelve un estado HTTP con sentido: 405 cuando el método no encaja, 415 cuando el tipo de medio no está soportado. Leer el cuerpo no es lo primero que haces, sino lo último, una vez que método y cabeceras te han confirmado que hay algo que merece la pena leer y que sabes cómo interpretarlo.

💡
No confíes en la cabecera a ciegas

El Content-Type lo pone el cliente, y un cliente puede mentir o equivocarse. La cabecera te da la intención declarada, pero el parseo es la verdad: por eso el try/catch alrededor de request.json() no es opcional en un endpoint público. Valida el resultado —forma, tipos, tamaño— en cuanto lo tengas deserializado, porque a partir de ahí ese dato conduce tu lógica.

Leer el cuerpo es un acto de compromiso, no de lectura

En casi todos los entornos donde has programado, el cuerpo de una petición era un dato: una cadena o un búfer que ya estaba en memoria y que podías mirar cuantas veces quisieras. En el edge eso se invierte, y la inversión es filosófica antes que técnica. El cuerpo no es un dato, es un proceso en curso: un flujo que la red entrega poco a poco y que solo existe en la medida en que lo consumes. Por eso se lee una sola vez, y no por una mezquindad del runtime: un flujo, por definición, no tiene un segundo pase que releer, porque leerlo es agotarlo. Esta regla, que al principio parece una trampa, es en realidad la que te obliga a la disciplina correcta: decidir antes de leer. El método y las cabeceras son el mapa que consultas mientras aún puedes echarte atrás; el instante en que llamas a json o a arrayBuffer es el instante en que te comprometes con una interpretación y con haber traído esos bytes a memoria. Esa frontera es oro en el edge, donde la memoria por isolate es escasa: te permite rechazar una subida de dos gigabytes por su Content-Length sin bufferizar un solo byte, o verificar una firma sobre el flujo antes de decidir si merece la pena parsearlo. Y es exactamente la misma disciplina que más adelante te dejará transformar gigabytes con kilobytes de memoria. La regla del pase único no es la limitación del streaming: es su condición de posibilidad. Cuando dejas de ver el cuerpo como una cosa que está ahí y empiezas a verlo como algo que sucede, has entendido por qué en el edge se lee con criterio y se lee una vez.

⚔️ Domina la lectura del cuerpo
  1. Escribe un Worker que acepte POST con JSON, lo deserialice con request.json() y lo devuelva con Response.json().
  2. Provoca a propósito el error de doble lectura leyendo text() y luego json(); después arréglalo clonando con request.clone().
  3. Despacha según el Content-Type: json, formData o text, y responde 415 para cualquier otro tipo.
  4. Rechaza los GET con un 405 antes de tocar el cuerpo, y explica por qué un GET no lleva cuerpo que leer.
  5. Recibe una subida binaria con arrayBuffer(), calcula su SHA-256 con crypto.subtle y responde el hash, sin decodificarla nunca como texto.