Article

El bucle de redirección que solo ocurre detrás de HTTPS

Una reescritura de configuración regional forzada a http: es de otro origen detrás de un proxy HTTPS, así que Next la reemite como solicitud externa que reentra en el proxy y deja la página de inicio en un bucle de 301. Pasa todas las pruebas locales porque el bucle solo aparece cuando X-Forwarded-Proto: https está establecido. La solución: marcar la reescritura interna con un secreto por proceso para que la canonicalización la omita.
July 14, 2026
Topics:Deployment
Tags:Next.jsCaddy

Lanzamos un cambio de configuración regional en un front end Next.js desacoplado y la página de inicio empezó a devolver HTTP 301, pero solo en producción, detrás del proxy inverso. En local funcionaba bien. Esta es la trampa, y la razón de una línea por la que se escondió de todas las pruebas locales.

La configuración

El inglés se sirve sin prefijo. Un proxy de Next (middleware) reescribe / hacia un árbol interno /en para que la barra de direcciones se mantenga limpia y, por separado, redirige con 301 cualquier acceso directo a /en/... hacia su ruta canónica sin prefijo, para que una página nunca tenga dos direcciones.

// /        -> internal rewrite to /en   (renders, 200)
// /en/foo  -> 301 -> /foo               (canonicalize)

Dos reglas que parecen independientes. No lo son.

La trampa

Una corrección anterior forzó el destino de la reescritura a http:. Detrás del proxy que termina TLS, la solicitud llega como https (vía X-Forwarded-Proto) mientras la aplicación escucha en HTTP plano. Reescribir hacia una URL https del mismo origen hacía que Next hiciera proxy por TLS hacia un puerto en texto plano —EPROTO, un 500 en todas las rutas—, así que forzar http: fue la corrección adecuada para ese error.

Pero http: mientras la solicitud entrante es https: es un origen distinto. Next trata una reescritura entre orígenes como una solicitud externa y la vuelve a emitir hacia el servidor. La solicitud reemitida vuelve a pasar por el proxy en /en, coincide con la regla de canonicalización y devuelve un 301 hacia /. La página de inicio es ahora un bucle de redirección.

Por qué todas las pruebas locales pasaron

Sin X-Forwarded-Proto: https, la solicitud es http plano, la reescritura es del mismo origen y se mantiene interna: sin reentrada, sin bucle. Un simple curl localhost:3000/ devuelve 200. El bucle aparece solo cuando algo establece la cabecera forwarded-proto, es decir, solo detrás del proxy real. La comprobación de salud de staging se había endurecido para enviar esa cabecera y exigir un 200; esa es la comprobación que lo detectó antes de producción.

La solución

La solicitud interna reemitida necesita saltarse la canonicalización. Una cabecera de marcador fija funcionaría, pero cualquier cliente externo podría suplantarla para eludir la redirección, y basarse en el protocolo no es fiable porque la reemisión lleva el https de aguas arriba. La respuesta es un secreto por proceso: marcar la reescritura interna con un valor que solo conoce el servidor en ejecución.

// generated once per process
const token = randomHex(24)

// canonicalization: skip only our own re-issue
if (isEnglishPrefixed && req.headers.get(MARKER) !== token) {
  return redirect(canonical, 301)
}

// on the internal rewrite, attach the token
requestHeaders.set(MARKER, token)

La reemisión interna se reconoce independientemente del protocolo reenviado, y un cliente externo no puede falsificar el token para escapar de la canonicalización. La página de inicio devuelve 200; un acceso genuino a /en/... sigue devolviendo 301 hacia su URL canónica.

Conclusiones

  • Una reescritura de middleware cuyo origen de destino difiere del origen de la solicitud es una solicitud externa, no interna, incluso cuando el host es idéntico. El protocolo forma parte del origen.
  • Detrás de un proxy que termina TLS, X-Forwarded-Proto cambia lo que significa "mismo origen". Pruebe la ruta a través del proxy, no solo el puerto de la aplicación.
  • Cuando dos reglas de enrutamiento comparten un espacio de rutas, demuestre que no pueden combinarse en un bucle, idealmente con una comprobación de salud que ejercite la condición real de forwarded-proto.