La intención de diseño de las cinco clases
El primer dígito del código determina la clase, y es deliberado: aunque el cliente no conozca un código concreto (por ejemplo el nuevo 425), puede tratarlo bien por su clase —4xx siempre significa «no reintentes tal cual, el problema está en la petición», 5xx significa «puedes reintentar más tarde» y 3xx significa «mira la cabecera Location»—. El RFC 9110 exige que el cliente entienda la semántica de la clase y permite que no entienda el código concreto. Por eso los códigos personalizados deben caer en el rango de centenas correcto.
Por qué 401 se llama Unauthorized si significa «sin autenticar»
Un error histórico de nomenclatura. En los primeros tiempos de HTTP no se distinguía entre authentication (quién eres) y authorization (qué puedes hacer); el 401 se llamó Unauthorized, pero la cabecera WWW-Authenticate que lo acompaña y su semántica de «puede funcionar si vuelves a enviar credenciales» hablan de autenticación. El RFC 9110, en el §15.5.2, ya lo redacta como "lacks valid authentication credentials". Recuerda una frase: 401 = inicia sesión, 403 = aunque inicies sesión, no puedes. Con eso, el frontend decide si redirige al login o muestra «sin permiso»; si se usa al revés, aparecen los «ya había iniciado sesión y me devuelve al login una y otra vez».
Los dos ejes de la redirección: permanente o temporal, conservando o no el método
Los cuatro códigos de redirección habituales forman una matriz de 2×2. El 301 y el 308 son permanentes (el navegador y los buscadores recuerdan la nueva dirección); el 302 y el 307 son temporales. Históricamente, el navegador convertía el POST en GET al tratar 301/302; el RFC 9110 reconoce esa realidad y ofrece con 307/308 versiones estrictas que «deben conservar el método y el cuerpo». Para redirigir una web a https, el 301 está bien; para migrar un endpoint de API, las peticiones POST necesitan 307 o 308, o el cuerpo se perderá.
Caché y 304
Mucha gente cree que el 304 «ahorra una petición». En realidad, el navegador sí envió al servidor una petición condicional con If-None-Match / If-Modified-Since, el servidor la comparó y respondió 304 sin cuerpo: lo que se ahorra es la transferencia, no el viaje de ida y vuelta. Ahorrar peticiones de verdad depende de Cache-Control: max-age (dentro de su validez el navegador no pregunta al servidor). Los dos trabajan juntos: dentro de max-age se usa directamente y, al caducar, una petición condicional renueva con un 304. En DevTools, "200 (from disk cache)" es lo primero y "304" es lo segundo.
Los tres hermanos de pasarela 502 / 503 / 504
Son la combinación de 5xx más habitual en producción y distinguirlos reduce directamente el ámbito del diagnóstico. El proxy inverso (Nginx, una pasarela, una CDN) no consigue conectar con el origen o el origen devuelve algo no válido: 502, mira si el proceso de la aplicación se ha caído; el origen indica claramente que está ocupado o en mantenimiento: 503, normalmente devuelto a propósito, revisa el health check y el cortocircuito; el origen tarda en responder: 504, revisa las consultas lentas y los tiempos de espera. Cloudflare divide estos tres casos en 520–526, donde 521 ≈ 502 (conexión rechazada), 522/524 ≈ 504 (tiempo de espera de conexión o de lectura) y 525/526 son problemas de la capa TLS.
Los códigos de estado y los códigos de negocio deben ser coherentes
El antipatrón más común es devolver 200 en todas las peticiones y poner el error en el campo code del cuerpo. Eso hace que la CDN cachee páginas de error, que las alertas de 5xx del sistema de monitorización no salten, que fetch().ok del navegador pierda sentido y que la política de reintentos no sepa qué hacer. Lo correcto es que el código HTTP exprese «qué ha pasado a nivel de petición» (la semántica del RFC 9110) y que el código de negocio del cuerpo exprese «el detalle a nivel de negocio»: reparto de tareas, no sustitución. Sin existencias es 422 + {"code":"OUT_OF_STOCK"}, no 200 + {"code":500}.