Saltar al contenido

Modificar la respuesta desde el middleware

El middleware no solo mira la petición a la ida: a la vuelta tiene la respuesta en la mano y puede transformarla. Leer el status y el cuerpo que devuelve next, retocar o añadir cabeceras sin renderizar de nuevo, reescribir el HTML construyendo una nueva Response, y cortar la cadena por completo con una redirección o una respuesta propia cuando la ruta no debe ni renderizarse.

next devuelve una Response que puedes leer y reescribir

Ya sabemos que next renderiza la ruta y devuelve su Response. Lo que aún no habíamos aprovechado es que esa respuesta es un objeto que puedes interrogar y transformar antes de que salga. Tiene un status, unas cabeceras y un cuerpo. En el tramo de vuelta del middleware —después de await next()— se abre una ventana para inspeccionar qué produjo la ruta y decidir si lo dejas pasar intacto o lo cambias.

TypeScript
export const onRequest = defineMiddleware(async (context, next) => {  const response = await next();  if (response.status === 404) {    console.warn('no encontrada:', context.url.pathname);  }  return response;});

Ese acceso es lo que convierte al middleware en algo más que un portero de entrada: lo vuelve también un revisor de salida. Puede leer el código de estado que la página emitió, mirar sus cabeceras, incluso consumir su cuerpo. Y como corre para todas las rutas, cualquier transformación que apliques aquí alcanza a la respuesta de cada una, desde un único sitio, sin repetir nada página por página.

Merece la pena nombrar las tres cosas que puedes hacer con esa respuesta en la mano, porque vertebran todo el capítulo: leerla sin cambiarla —inspeccionar su status o sus cabeceras para decidir o registrar—, retocarla —añadir cabeceras, reescribir el cuerpo— y sustituirla por otra que construyes tú. Las tres conviven en un mismo onRequest, y elegir bien entre ellas es casi todo el oficio.

Tocar cabeceras sin renderizar de nuevo

La modificación más común y más barata es sobre las cabeceras. La Response que devuelve next trae un objeto de headers mutable: puedes añadir, cambiar o borrar cabeceras sin volver a renderizar nada, porque el cuerpo ya está hecho y solo estás retocando el sobre que lo envuelve.

TypeScript
export const onRequest = defineMiddleware(async (context, next) => {  const response = await next();  response.headers.set('X-Frame-Options', 'DENY');  response.headers.set('X-Content-Type-Options', 'nosniff');  return response;});

Este es el patrón canónico para las cabeceras de seguridad: las fijas una vez en el middleware y protegen todas las páginas del sitio a la vez, sin repetir una línea en cada layout. Si en cambio quieres reescribir el cuerpo —inyectar una marca, reemplazar un fragmento—, ya no basta con retocar el sobre: hay que leer el texto de la respuesta y construir una nueva con ese contenido, conservando estado y cabeceras.

TypeScript
export const onRequest = defineMiddleware(async (context, next) => {  const response = await next();  const html = await response.text();  const nuevo = html.replace('</body>', '<!-- servido al vuelo --></body>');  return new Response(nuevo, {    status: response.status,    headers: response.headers,  });});

Reescribir el cuerpo es más caro —obliga a materializar todo el HTML en memoria y a levantar una Response nueva— así que se reserva para cuando de verdad hace falta. Retocar cabeceras, en cambio, es ligerísimo, y por eso es lo que más verás en un middleware real.

Cortar la cadena: redirigir o responder sin renderizar

Hasta aquí dejábamos que next renderizara y luego tocábamos el resultado. Pero el middleware también puede decidir que la ruta no se renderice en absoluto: basta con no llamar a next y devolver otra cosa. Esa es la operación de cortar la cadena, y es la base de toda protección de acceso.

La forma más habitual es redirigir. Si la petición no cumple una condición —no hay sesión para una zona privada— devuelves context.redirect hacia el login, y la ruta pedida ni se toca:

TypeScript
export const onRequest = defineMiddleware((context, next) => {  const privado = context.url.pathname.startsWith('/panel');  if (privado && !context.locals.usuario) {    return context.redirect('/entrar');  }  return next();});

Cuando entras en esa rama, next nunca se llama: no hay renderizado, no hay coste, la petición se resuelve con un 30x y una cabecera Location. Como alternativa puedes devolver una Response construida a mano —un 403 seco para una API, un cuerpo de mantenimiento— y el efecto es el mismo: la cadena se detiene ahí.

TypeScript
if (bloqueado) {  return new Response('Acceso prohibido', { status: 403 });}

Hay un matiz que cobrará todo su sentido en el próximo capítulo, cuando encadenemos varios middlewares: cortar sin llamar a next no solo se salta el renderizado de la ruta, sino también cualquier middleware que viniera después. El corte es total, no parcial; detiene todo el procesamiento pendiente de esa petición. Por eso a un guardián que veta el acceso le conviene ir pronto en la cadena, porque cuanto antes corte, más trabajo inútil ahorra a los que le siguen.

La regla mental que ordena todo esto es limpia: llamar a next es dejar pasar; no llamarlo es cortar. A la ida decides si cortas —redirigiendo o respondiendo tú—; a la vuelta, si dejaste pasar, decides cómo retocar la respuesta que salió. Entre esas dos capacidades cabe casi toda la lógica transversal de un sitio.