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.
El middleware que hemos visto se limitaba a dejar pasar: llamaba a next y devolvía su resultado sin tocarlo. Pero esa Response que next entrega es un objeto vivo que llega a tus manos antes de salir al navegador, y ahí hay poder de sobra. En el tramo de vuelta puedes leer qué código de estado emitió la página, retocar sus cabeceras, incluso reescribir su cuerpo. Y a la ida puedes decidir algo más radical: que la ruta ni se renderice, cortando la cadena con una redirección o una respuesta que construyes tú. Observar y transformar la respuesta, o impedir que exista: ese es el mando que este capítulo pone en tus manos.
- Leer el
status, las cabeceras y el cuerpo de laResponseque devuelvenext. - Añadir o cambiar cabeceras sobre la respuesta ya renderizada.
- Reescribir el cuerpo construyendo una nueva
Response. - Cortar la cadena con
context.redirecto unaResponsepropia sin renderizar.
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.
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.
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.
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:
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í.
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.
flowchart TD
RQ[peticion entra] --> D{cumple la condicion}
D -->|no| CUT[corta sin llamar a next]
CUT --> RD[redirect o Response propia]
D -->|si| NX[llama a next]
NX --> RS[respuesta renderizada]
RS --> HD[retoca cabeceras o cuerpo]
HD --> OUT[respuesta final]
style RQ fill:#89b4fa,color:#11111b
style RD fill:#f38ba8,color:#11111b
style OUT fill:#a6e3a1,color:#11111bEl objeto context del middleware trae las mismas herramientas de enrutado que una página: context.redirect para mandar al visitante a otra URL con un 30x, y context.rewrite para servir el contenido de otra ruta sin mover la barra de direcciones. La diferencia con usarlas en una página es el alcance: aquí una sola decisión cubre familias enteras de rutas. Un context.rewrite('/mantenimiento') en el middleware desvía por dentro todo un sitio; un context.redirect('/entrar') protege toda una zona privada de golpe.
El cuerpo de una Response es un flujo que se lee una sola vez. En cuanto haces await response.text(), esa respuesta queda vaciada y ya no puedes devolverla tal cual: si lo intentas, el navegador recibe un cuerpo agotado. Por eso, siempre que consumas el cuerpo para transformarlo, tienes que construir una Response nueva con el texto resultante y arrastrar el status y los headers de la original. Retocar solo cabeceras no tiene este problema —no toca el cuerpo—; reescribir contenido, sí, y olvidarlo produce páginas en blanco desconcertantes.
Si tu intención es únicamente añadir o quitar cabeceras, no reconstruyas la respuesta: basta con mutar response.headers sobre la que devuelve next, que es lo más barato que hay. Reserva el patrón de construir una Response nueva para cuando de verdad reescribes el cuerpo. Distinguir los dos casos —el sobre con el contenido intacto frente al contenido transformado— te ahorra materializar en memoria un HTML que no ibas a tocar.
Revisor de salida
A la vuelta lees el status y las cabeceras que la ruta emitio y decides si pasan intactos o cambian.
Cabeceras baratas
response.headers.set retoca el sobre sin renderizar de nuevo. Ideal para seguridad en todo el sitio.
Cortar la cadena
No llamar a next detiene el render. Devuelves un redirect o una Response propia y la ruta ni se toca.
Reescribir el cuerpo
Lee el texto y levanta una Response nueva con el status y las cabeceras de la original.
Hay una simetría honda en la posición del middleware que conviene ver de frente. Se sienta justo en el borde de tu aplicación, y desde ahí toca la petición dos veces con dos naturalezas opuestas. A la entrada actúa como guardián: mira lo que llega y decide si merece renderizarse, con el poder de vetarlo por completo antes de gastar un solo ciclo en la página. A la salida actúa como editor: recibe lo que la aplicación produjo y tiene la última palabra sobre cómo se presenta al mundo, con el poder de reescribir cabeceras y cuerpo antes de que nadie lo vea. Guardián a la ida, editor a la vuelta: la misma función, dos oficios, y next como la frontera que los separa. Esta forma no es un capricho de Astro, es un arquetipo que reaparece en todo sistema que procesa un flujo —los filtros de un servidor, los interceptores de una red, los envoltorios de una función—, y su fuerza viene de una idea sencilla y radical: que la petición y la respuesta son valores de primera clase, cosas que puedes tener en una variable, inspeccionar, comparar, transformar y sustituir, y no eventos opacos que solo puedes dejar suceder. En cuanto tratas la respuesta como un valor que te devuelven y no como un efecto que se escapa, el abanico de lo posible se abre: puedes medirla, envolverla, reemplazarla, cortarla antes de nacer. Pero ese poder viene con una disciplina inseparable, y es el hilo que recorre este capítulo: pase lo que pase en tu onRequest, tienes que entregar una Response coherente. Si cortas, devuelve una respuesta completa y honesta. Si dejas pasar y retocas, no rompas lo que ya estaba bien. Si consumes el cuerpo, reconstruye uno nuevo. El middleware te da las llaves del borde de tu sitio; ser digno de ellas es recordar que, al final de cada camino, alguien espera al otro lado una respuesta bien formada.
- Añade
X-Frame-OptionsyX-Content-Type-Optionsa todas las respuestas retocandoresponse.headersa la vuelta denext. - Lee
response.statusy registra en la terminal cada vez que una ruta emita un404. - Protege
/panel: si no haycontext.locals.usuario, devuelvecontext.redirect('/entrar')sin llamar anext, y comprueba que la página privada no llega a renderizarse. - Reescribe el cuerpo para inyectar un comentario justo antes de
</body>, construyendo unaResponsenueva, y verifica que elstatusoriginal se conserva.