Una redirección parece sencilla: el servidor responde con un código de estado y un encabezado Location, y el cliente va a esa dirección. Pero esos tres dígitos deciden si los navegadores pueden guardar la redirección en caché, si un POST sigue siendo un POST y qué URL es probable que indexen los motores de búsqueda.

Esta guía repasa la clase 3xx tal como la define la RFC 9110, la historia que explica sus solapamientos, las redirecciones sin código de estado y cómo configurar el código correcto en los servidores más habituales.

Qué significa la clase 3xx

La RFC 9110, la especificación vigente de la semántica de HTTP, dice que un código de estado 3xx indica que “el agente de usuario debe realizar alguna acción adicional para completar la solicitud”. La sección 15.4 agrupa los códigos en cuatro tipos de redirección:

  • El recurso está disponible en otra URI, indicada en Location: 301, 302, 307 y 308.
  • El cliente puede elegir entre varias representaciones del recurso: 300.
  • Otro recurso proporciona una respuesta indirecta a la solicitud: 303.
  • El cliente puede reutilizar una copia que ya tiene en su caché: 304.

En la práctica, solo 301, 302, 303, 307 y 308 funcionan como redirecciones automáticas. El Fetch Standard, que implementan los navegadores, llama precisamente a estos cinco códigos “redirect statuses” (estados de redirección). Un 300 suele mostrarse como una página normal, y un 304 le indica al cliente que use la copia que tiene en caché.

El valor de Location puede ser una referencia relativa, como /new-page o ../shop/. El cliente la resuelve a partir de la URL que acaba de solicitar (RFC 9110, sección 10.2.2). Si Location no tiene fragmento, se conserva el original: un enlace a /guide#install que redirige a /docs/guide termina en /docs/guide#install.

Para las cinco redirecciones propiamente dichas, dos preguntas determinan qué código conviene:

  1. ¿El cambio es permanente o temporal? Las redirecciones permanentes pueden almacenarse en caché e indican a los motores de búsqueda que indexen la nueva URL en lugar de la antigua.
  2. ¿Hay que conservar el método de la solicitud? Importa cuando la solicitud original es un POST, PUT o DELETE y no un simple GET.

Los códigos de redirección de un vistazo

Código Nombre Permanente Conserva el método Cacheable por defecto Uso típico Búsqueda de Google
300 Multiple Choices – – Sí Ofrecer representaciones alternativas (poco habitual) –
301 Moved Permanently Sí No: POST puede pasar a GET Sí Páginas y sitios trasladados, canonicalización de HTTPS y del host Permanente (señal fuerte)
302 Found No No: POST puede pasar a GET No Redirecciones de corta duración, muros de inicio de sesión, pruebas A/B Temporal (señal débil)
303 See Other No No: pasa a GET No Página de resultados tras enviar un formulario Temporal
304 Not Modified No es una redirección – – Respuesta a una solicitud condicional Contenido sin cambios
305 Use Proxy Obsoleto – – No usar –
306 (Unused) Reservado – – No usar –
307 Temporary Redirect No Sí No Redirecciones temporales con cualquier método Temporal
308 Permanent Redirect Sí Sí Sí Traslados permanentes de API, formularios o cualquier URL Permanente (señal fuerte)

“Cacheable por defecto” significa que el código es cacheable de forma heurística según la RFC 9110, sección 15.1: una caché puede reutilizar la respuesta sin consultar al servidor, aunque no incluya información explícita de vigencia. Aun así, cualquier respuesta puede hacerse cacheable o no cacheable con Cache-Control. La columna de Google resume la documentación de Google sobre redirecciones y códigos de estado HTTP.

Los códigos, uno por uno

300 Multiple Choices

El recurso tiene varias representaciones, como distintos idiomas o formatos de archivo, y el cliente puede elegir. El cuerpo suele enumerar las opciones, sin un formato estándar. Se puede indicar una opción preferida en Location, pero no puedes contar con que los navegadores la sigan. En la práctica, el 300 casi nunca se usa: funcionan mejor la negociación de contenido en el servidor o una página normal con enlaces.

301 Moved Permanently

Al recurso de destino “se le ha asignado una nueva URI permanente, y cualquier referencia futura a este recurso debería usar una de las URI incluidas”. Es el código estándar para páginas trasladadas, cambios de dominio, el paso de HTTP a HTTPS y las redirecciones al host canónico.

Hay dos detalles importantes. Un 301 es cacheable de forma heurística, así que los navegadores pueden recordarlo y dejar de consultar al servidor (consulta la sección sobre la caché, más abajo). Y, por motivos históricos, un cliente puede convertir un POST en un GET al seguir un 301, y los navegadores lo hacen. Para las páginas que se cargan con GET esto no importa; para el endpoint de un formulario o una API, usa 308.

302 Found

El recurso de destino “reside temporalmente en otra URI”, así que los clientes deben seguir usando la URL original en las solicitudes futuras. Un 302 no es cacheable por defecto. Úsalo para situaciones pasajeras: una página de ofertas que sustituye a una categoría durante una semana, un muro de inicio de sesión, el enrutamiento geográfico o una prueba A/B.

Igual que el 301, el 302 permite el cambio de POST a GET. Muchos frameworks envían un 302 cuando no se indica ningún código, y por eso tantos traslados permanentes acaban, por descuido, como redirecciones 302.

303 See Other

Un 303 envía al cliente a otro recurso que sirve como respuesta indirecta a la solicitud original, y el cliente lo obtiene con GET (o HEAD). Está pensado para el patrón Post/Redirect/Get: después de procesar el envío de un formulario, el servidor responde con un 303 y la URL de una página de confirmación o de resultados. Si luego se recarga esa página, se repite un GET inofensivo en lugar de volver a enviar el formulario.

Un 303 no es cacheable por defecto y no afirma que la URL original se haya trasladado. Google lo trata como un 302.

304 Not Modified (no es una redirección)

Pese a su número 3xx, un 304 nunca envía al cliente a otra parte. Responde a una solicitud GET o HEAD condicional, es decir, una que incluye If-None-Match con un valor de ETag, o If-Modified-Since con una fecha. Si el recurso no ha cambiado, el servidor responde 304 sin cuerpo y el cliente usa la copia que tiene en caché.

HTTP
GET /styles.css HTTP/1.1
Host: example.com
If-None-Match: "5f1c-3a2b"

HTTP/1.1 304 Not Modified
ETag: "5f1c-3a2b"
Cache-Control: max-age=3600

Los verificadores de redirecciones envían solicitudes incondicionales, así que normalmente nunca ven un 304. Para probar uno en TrueURL, haz una verificación normal, copia el valor de ETag de sus encabezados de respuesta y envíalo como encabezado If-None-Match en Encabezados de solicitud personalizados. Para Googlebot, un 304 significa que el contenido no ha cambiado desde el último rastreo.

305 Use Proxy y 306 (Unused)

Un 305 indicaba al cliente que repitiera la solicitud a través del proxy indicado en Location. Permitir que una respuesta cambie la configuración del proxy de un cliente es un riesgo de seguridad, así que la RFC 7231 declaró obsoleto el código, y los navegadores actuales no le hacen caso.

El 306 se definió en una versión anterior de la especificación (a menudo aparece como Switch Proxy) y hoy está reservado. Ninguno de los dos códigos tiene cabida en una aplicación moderna; si ves uno, algo está mal configurado.

307 Temporary Redirect

Un 307 significa lo mismo que un 302, con una condición adicional: el cliente no debe cambiar el método de la solicitud. Un POST se reenvía como POST, con el mismo cuerpo, a la nueva URL. Por eso el 307 es la redirección temporal adecuada para API y endpoints de formularios.

También puedes encontrarte el 307 en las DevTools de Chrome como un 307 Internal Redirect de http:// a https://. El servidor nunca lo envió. Fue el propio navegador el que cambió la solicitud a HTTPS, normalmente por una política HSTS que tiene guardada para ese sitio.

308 Permanent Redirect

Un 308 es el equivalente del 301 que conserva el método: es permanente, es cacheable de forma heurística y el cliente no debe cambiar el método. Se publicó por primera vez en 2014 en la RFC 7238, de carácter experimental, se convirtió en estándar con la RFC 7538 en 2015 y hoy forma parte de la RFC 9110.

Usa 308 para traslados permanentes de endpoints que reciben solicitudes POST, PUT o DELETE. Para páginas normales, 301 y 308 son intercambiables, y Google trata ambos como redirecciones permanentes.

Por qué existen 303, 307 y 308

El solapamiento entre 301/302 y 303/307/308 es un accidente histórico.

HTTP/1.0 (RFC 1945, 1996) definió 301 Moved Permanently y 302 Moved Temporarily. Establecía que un cliente no debía redirigir un POST automáticamente sin la confirmación del usuario, y ya señalaba que algunos clientes convertían por error el POST en un GET. Los navegadores siguieron haciéndolo.

HTTP/1.1 agregó dos códigos que no dejan lugar a interpretaciones. La RFC 2068 (1997) introdujo 303 See Other, que siempre cambia a GET. La RFC 2616 (1999) agregó 307 Temporary Redirect, que siempre conserva el método, cambió el nombre del 302 a Found y reconoció abiertamente que la mayoría de los clientes trataban un 302 como un 303.

Aun así, seguía sin existir una redirección permanente que conservara el método. El 308 llenó ese hueco quince años después. Mientras tanto, la especificación aceptó la realidad: desde la RFC 7231 (2014), un cliente puede convertir un POST en un GET después de un 301 o un 302.

Hoy, los navegadores aplican estas reglas del Fetch Standard:

Código Método original Los navegadores solicitan la nueva URL con
301, 302 POST GET, sin el cuerpo
301, 302 Cualquier otro método El mismo método
303 Cualquier método excepto GET y HEAD GET, sin el cuerpo
303 GET o HEAD El mismo método
307, 308 Cualquier método El mismo método y el mismo cuerpo

Solo el POST se reescribe después de un 301 o un 302; un PUT o un DELETE conserva su método. Confiar en eso es arriesgado, porque las bibliotecas HTTP no se comportan todas igual, así que usa 307 o 308 siempre que el método importe.

Redirecciones sin código de estado 3xx

Meta refresh

HTML
<meta http-equiv="refresh" content="0; url=https://example.com/new-page">

La página en sí se entrega con 200 OK, y el navegador sigue la etiqueta después de cargarla. El número es el retraso en segundos. Google interpreta un meta refresh instantáneo como una redirección permanente y uno con retraso como una redirección temporal.

Las actualizaciones temporizadas también perjudican la accesibilidad. Las WCAG aceptan un meta refresh instantáneo como redirección del lado del cliente (técnica H76), pero consideran un fallo uno temporizado (F40), porque la página cambia según un temporizador que el usuario no puede controlar.

Redirecciones JavaScript

HTML
<script>
  window.location.replace('https://example.com/new-page');
</script>

location.replace() no deja la página que redirige en el historial de la sesión, así que el botón Atrás no hace que los usuarios vuelvan a caer en la redirección. Asignar location.href, en cambio, sí la deja ahí.

Google solo ejecuta JavaScript cuando renderiza una página, después de rastrearla, y el renderizado puede fallar. Su documentación recomienda las redirecciones JavaScript solo cuando no es posible usar redirecciones del lado del servidor ni meta refresh. Muchos otros bots y herramientas no ejecutan JavaScript en absoluto.

El encabezado Refresh

HTTP
HTTP/1.1 200 OK
Content-Type: text/html
Refresh: 0; url=https://example.com/new-page

El encabezado Refresh no está definido en ningún RFC de HTTP. Los navegadores lo admiten desde hace décadas y el HTML Standard lo trata como equivalente a una etiqueta meta refresh. Muchos clientes y herramientas HTTP, entre ellos curl, lo ignoran, así que es preferible una redirección 3xx de verdad.

Cómo implementar correctamente las redirecciones

nginx

nginx
# Toda solicitud http://, en cualquiera de los dos hosts, va a la URL HTTPS canónica en un solo salto
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

# https://www.example.com -> https://example.com
server {
    listen 443 ssl;
    server_name www.example.com;
    ssl_certificate     /etc/ssl/example.com.crt;
    ssl_certificate_key /etc/ssl/example.com.key;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;
    # ssl_certificate y ssl_certificate_key, como arriba

    # Una página trasladada
    location = /old-page {
        return 301 /new-page;
    }

    # Una sección completa, conservando el resto de la ruta
    location /blog/ {
        rewrite ^/blog/(.*)$ /articles/$1 permanent;
    }
}

$request_uri contiene la ruta y la cadena de consulta originales, así que no se pierde nada. rewrite ... permanent envía un 301 y rewrite ... redirect, un 302; para un 307 o un 308, usa return con ese código.

Apache (.htaccess)

Apache
# mod_alias: una página y una carpeta completa (se agrega el resto de la ruta)
Redirect 301 /old-page https://example.com/new-page
Redirect 301 /blog/ https://example.com/articles/

# mod_alias con una expresión regular
RedirectMatch 301 ^/products/([0-9]+)\.html$ https://example.com/p/$1

# mod_rewrite: envía toda solicitud http:// y www a https://example.com en un solo salto
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

Redirect sin código de estado envía un 302, así que escribe siempre el código. Detrás de un balanceador de carga o una CDN que se conecta a Apache por HTTP sin cifrar, %{HTTPS} vale siempre off y la última regla entra en bucle; comprueba %{HTTP:X-Forwarded-Proto} en su lugar. mod_alias y mod_rewrite funcionan de forma independiente, y el orden de las líneas en el archivo no decide qué módulo se ejecuta primero. Una URL que coincide con reglas de ambos, como http://www.example.com/old-page en este ejemplo, puede necesitar dos saltos; para las URL que deben redirigir en un solo salto, mantén todas sus reglas en un mismo módulo.

Cloudflare

Cloudflare puede redirigir en su red perimetral, antes de que la solicitud llegue a tu servidor:

  • Con Single Redirects (la sección Redirect Rules del panel), las solicitudes se seleccionan con una expresión de filtro y se envían a un destino estático o dinámico con 301, 302, 303, 307 o 308. Una regla típica de www al dominio raíz tiene esta configuración:

    When incoming requests match:  (http.host eq "www.example.com")
    URL redirect type:             Dynamic
    Expression:                    concat("https://example.com", http.request.uri.path)
    Status code:                   301
    Preserve query string:         On
  • Con Bulk Redirects se gestionan listas largas de URL de origen y destino, como un mapa de migración subido en CSV. Cada entrada tiene su propio código de estado y opciones como conservar la cadena de consulta, y una Bulk Redirect Rule activa la lista.

  • Con Workers puedes crear redirecciones en código con Response.redirect():

JavaScript
export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === '/old-page') {
      url.pathname = '/new-page'; // se conserva la cadena de consulta
      return Response.redirect(url.toString(), 301);
    }
    return fetch(request);
  },
};

Response.redirect() usa 302 por defecto y solo acepta 301, 302, 303, 307 y 308. Cualquier otro código lanza un RangeError.

Node.js y Express

JavaScript
import express from 'express';

const app = express();
app.set('trust proxy', 1); // si se ejecuta detrás de un balanceador de carga o una CDN

// HTTPS y host canónico en un solo salto
app.use((req, res, next) => {
  if (req.secure && req.hostname === 'example.com') return next();
  res.redirect(301, `https://example.com${req.originalUrl}`);
});

// Una página trasladada
app.get('/old-page', (req, res) => {
  res.redirect(301, '/new-page');
});

res.redirect(url) sin código de estado envía un 302. Express 5 solo admite el orden res.redirect(status, url). En Node.js puro, define tú mismo el código de estado y el encabezado:

JavaScript
res.writeHead(301, { Location: 'https://example.com/new-page' });
res.end();

PHP

PHP
<?php
// Debe ejecutarse antes de enviar cualquier salida.
header('Location: https://example.com/new-page', true, 301);
exit;

Un encabezado Location por sí solo hace que PHP envíe un 302 (salvo que ya se haya establecido un código 201 o 3xx), así que pasa el código como tercer argumento y llama a exit para que no se ejecute nada más. En WordPress, wp_redirect() y wp_safe_redirect() también usan 302 por defecto; pasa 301 como segundo argumento.

Cómo elegir el código de redirección adecuado

  • La página se ha trasladado definitivamente y es una página normal que se carga con GET: usa 301. Un 308 funciona igual de bien.
  • El traslado es permanente y los clientes pueden enviar POST, PUT o DELETE, como ocurre con las API, las acciones de formularios y los webhooks: usa 308.
  • La redirección es temporal y solo importa GET: usa 302.
  • La redirección es temporal y hay que conservar el método: usa 307.
  • Se ha procesado un formulario u otro POST y el usuario debe ver una página de resultados: usa 303.
  • De HTTP a HTTPS: usa 301 o 308, y envía un encabezado Strict-Transport-Security desde el sitio HTTPS para que los navegadores se salten la solicitud insegura la próxima vez.
  • El contenido ya no existe y no tiene equivalente: no redirijas. Devuelve 404 o 410. Google puede tratar una redirección a una página no relacionada, como la página de inicio, como un soft 404.
  • 300, 305 y 306: no los uses. El 304 lo genera tu servidor o tu caché ante solicitudes condicionales y nunca es una opción de redirección.

La trampa de la caché con 301 y 308

Como 301 y 308 son cacheables de forma heurística, los navegadores pueden almacenarlos y dejar de consultar al servidor. Los navegadores basados en Chromium conservan un 301 o un 308 sin encabezados de caché hasta que la entrada se descarta o el usuario borra la caché. Una redirección permanente errónea puede seguir enviando a los visitantes anteriores al lugar equivocado mucho después de que corrijas el servidor, y no hay una forma fiable de retirarla de sus navegadores.

Para evitar problemas:

  • Prueba primero las reglas nuevas con 302 o 307, y cambia a 301 o 308 cuando el destino esté confirmado.
  • Otra opción es limitar cuánto tiempo puede almacenarse en caché la redirección permanente mientras la despliegas:
nginx
location = /old-page {
    add_header Cache-Control "max-age=3600";
    return 301 https://example.com/new-page;
}
  • Al hacer pruebas, usa una ventana privada o las DevTools con Disable cache activado, o un verificador del lado del servidor, que nunca usa la caché de un navegador.
  • Purga las cachés de la CDN y de los proxies después de cambiar una redirección, porque también pueden almacenar redirecciones.
  • No intentes deshacer una redirección A → B almacenada en caché redirigiendo B de vuelta a A. Los visitantes cuyo navegador guardó la primera redirección pueden acabar en un bucle.

Cómo probar tus redirecciones

Sea cual sea tu plataforma, verifica el resultado desde fuera. El Verificador de redirecciones 301 confirma que cada salto de un traslado permanente es un 301 o un 308; el Verificador de cadenas de redirecciones muestra cada salto con su Location y sus encabezados, y el Verificador de redirecciones HTTPS prueba a la vez las cuatro variantes de protocolo y host. El Verificador de meta refresh detecta las redirecciones del lado del cliente que se les escapan a las herramientas que solo leen encabezados, y el Medidor de latencia de redirecciones mide cuánto cuesta cada salto.

Desde una terminal, curl -sIL https://example.com/old-page muestra los encabezados de cada salto. Nuestra guía sobre cómo verificar redirecciones de URL explica en detalle la línea de comandos, las DevTools y los problemas de redirección más frecuentes.