1. Ruta y método: 404 vs 405
Es muy fácil confundir 404 y 405, porque desde el punto de vista de un principiante ambas situaciones se ven igual: «la solicitud no se procesó de todas formas». Pero en HTTP son dos preguntas diferentes, y el servidor debe responderlas de manera diferente. Imagina que path es la dirección de un apartamento, y method es la acción: tocar el timbre, entregar un paquete, derribar una pared con un taladro.
Si el cliente llega a una dirección que no existe, es 404 Not Found: «no conocemos a nadie en esa dirección». Si la dirección existe pero la acción no está permitida, es 405 Method Not Allowed: «el apartamento existe, pero aquí no se puede usar el taladro; aquí está la lista de acciones permitidas». Y esa lista se expresa precisamente mediante el encabezado Allow.
Para que quede más claro, tomemos nuestra API ReadLater. La ruta /api/v1/reading-list existe, y para ella son adecuados GET (obtener lista) y POST (crear elemento). Si el cliente envía PUT /api/v1/reading-list, no es «ruta no encontrada», es «método no soportado para esta ruta», es decir, 405, no 404.
2. Utilidad de 405 para el cliente
A nivel educativo se podría decir: «¿Qué diferencia hay? Devuelve un 404 y ya». Pero en el mundo real, 405 es uno de esos pequeños detalles que hacen que una API sea predecible y fácil de usar. Un cliente, al ver 405, entiende: «no me equivoqué de dirección, sino de acción». Esto acelera mucho la depuración, especialmente cuando las solicitudes se construyen mediante programación y no a mano en Postman.
Hay otro beneficio: cuando devuelves 405 junto con Allow, literalmente le das al cliente una pista de «cómo hacerlo bien». Para una persona, es como un cartel en la puerta: «Recepción abierta: L–V 10:00–18:00». Incluso si igual no lo dejas entrar, haces que el comportamiento del servidor sea amigable y explicable (algo raro pero agradable).
Ahora un punto práctico importante: 405 ocurre antes de leer el body y la validación. Si el método no está permitido, no tiene sentido leer el JSON, no hay que llamar a ObjectMapper, no hay que ejecutar el validador. De lo contrario, podrías obtener una situación extraña: el cliente envió un método incorrecto y el servidor le responde «JSON roto» o «title obligatorio». Formalmente podría ser cierto, pero en sentido, es una respuesta a la pregunta equivocada.
3. Allow y su relación con 405
Allow es un encabezado HTTP que informa al cliente la lista de métodos permitidos para un recurso/ruta específico. En nuestro curso, nos interesa una relación muy simple: si devuelves 405 Method Not Allowed, casi siempre debes agregar Allow y enumerar allí los métodos que esa ruta realmente soporta.
La disciplina aquí es: Allow se refiere a la ruta específica, no «a toda la aplicación». Si /health solo soporta GET, entonces Allow debe ser GET. Si /api/v1/reading-list/{id} soporta GET, PUT, DELETE, entonces Allow debe contener exactamente esos, incluso si en alguna otra parte de la aplicación hay PATCH para el estado.
En nuestra API también mantendremos un contrato de error unificado: incluso para 405 devolveremos un JSON ErrorResponse, para que el cliente no tenga que escribir diferentes analizadores de errores. Allow sigue siendo una «pista de transporte» en los encabezados, como un letrero en la entrada, y el JSON es una explicación clara en palabras.
Un pequeño fragmento de código, que normalmente parece muy aburrido (y por eso muy correcto), es el siguiente:
import java.util.Set;
String allowHeaderValue(Set<String> allowedMethods) {
// En HTTP los métodos en Allow se enumeran separados por coma y espacio: "GET, POST"
return String.join(", ", allowedMethods);
}
Aquí no hay magia: según el estándar, los métodos se enumeran separados por coma. Sí, es uno de esos raros casos en los que «unir cadenas» es justo lo que hay que hacer.
4. Rutas de ReadLater y métodos
Para responder correctamente con 405, el servidor necesita saber: «si la ruta es reconocida, ¿qué métodos están permitidos para ella?». En sistemas grandes esto se almacena en el enrutador/framework, pero nosotros tenemos Java plano y enrutamiento manual, por lo que lo hacemos explícito y simple. Para una API de aprendizaje, es perfectamente normal tener una pequeña «tabla de verdad» directamente en el código.
A continuación, nuestro mapa de rutas objetivo. Obsérvese que describimos patrones de rutas: {id} significa «un segmento cualquiera después de reading-list/», y puede no ser numérico. Esto es importante: queremos distinguir 405 de 400, y 400 por un id no válido se resolverá más adelante.
| Ruta (patrón) | Métodos permitidos | Significado breve |
|---|---|---|
| /health | GET | Verificar si el servidor está vivo |
| /api/v1/reading-list | GET, POST | Lista y creación |
| /api/v1/reading-list/{id} | GET, PUT, DELETE | Lectura/actualización/eliminación por id |
| /api/v1/reading-list/{id}/status | PATCH | Actualización parcial del estado |
Con esta tabla se pueden comprobar mentalmente varias situaciones. Por ejemplo, GET /api/v1/reading-list/1/status debería ser 405 (la ruta existe), mientras que GET /api/v1/reading-list/1/status/extra sería 404 (la ruta ya es diferente).
5. allowedMethods(path): reconocimiento de ruta
Ahora viene un matiz importante «para adultos». Queremos reconocer la ruta de modo que "/api/v1/reading-list/abc" siga considerándose «ruta de elemento», de lo contrario nunca podríamos devolver un 400 correcto por un id no numérico. Por lo tanto, para el enrutamiento por path verificamos la estructura de segmentos, pero no validamos su significado (por ejemplo, que id sea número). Las validaciones semánticas las dejamos para el paso de «análisis/validación».
A continuación, un ejemplo de un resolvedor muy didáctico pero útil. Presta atención al patrón [^/]+: significa «un segmento que no contiene /». Es decir, «cualquier cosa, siempre que no esté vacío ni se divida en dos segmentos».
import java.util.Set;
Set<String> allowedMethods(String path) {
// Primero, las coincidencias más simples y exactas
if ("/health".equals(path)) return Set.of("GET");
if ("/api/v1/reading-list".equals(path)) return Set.of("GET", "POST");
// "{id}" es exactamente un segmento, no necesariamente un número (el significado se comprobará después)
if (path.matches("/api/v1/reading-list/[^/]+")) return Set.of("GET", "PUT", "DELETE");
// Endpoint de estado — ruta separada
if (path.matches("/api/v1/reading-list/[^/]+/status")) return Set.of("PATCH");
// Ruta no reconocida — significa «ruta no encontrada» (luego se convertirá en 404)
return Set.of();
}
¿Por qué esto no es un «framework casero»? Porque no estamos construyendo un sistema universal de rutas, no hacemos anotaciones, no usamos reflexión, no inventamos un DSL. Simplemente codificamos honestamente cuatro reglas para cuatro rutas de nuestra API educativa. Es exactamente el nivel de simplicidad que sigue siendo útil, no peligroso.
6. En el handler: 404/405 antes del body y la validación
El resultado más importante de la lección es el orden de las comprobaciones. Primero decidimos si la ruta existe, luego si el método está permitido, y solo después comenzamos las operaciones «costosas»: lectura del body, ObjectMapper.readValue(...), validación del DTO, llamada al service y repository. Este orden hace que la API sea lógicamente correcta y evita errores extraños como «devolvemos validación aunque el método ni siquiera está soportado».
A continuación, un fragmento compacto de cómo suele verse el inicio de handle(...) en nuestro ReadingListHttpHandler. Aquí ya se siente el «ritmo backend»: determinar rápidamente las condiciones de transporte, y solo si están bien, pasar a los pasos de negocio.
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.util.Set;
void handleRoutingGuard(HttpExchange exchange, String path) throws IOException {
// Método de la solicitud (GET/POST/PUT/...)
String method = exchange.getRequestMethod();
// Lista de métodos permitidos para la ruta específica (si está vacía, la ruta no es reconocida)
Set<String> allowed = allowedMethods(path);
// Primero respondemos a la pregunta "¿existe la ruta?"
if (allowed.isEmpty()) {
writeNotFound(exchange);
return;
}
// Luego, "¿está permitido el método para esta ruta?"
if (!allowed.contains(method)) {
writeMethodNotAllowed(exchange, allowed);
return;
}
// Importante: aquí todavía no hemos leído el body ni validado el DTO
}
Observa dos cosas. Primero, no estamos leyendo exchange.getRequestBody() en absoluto. Segundo, writeNotFound(...) y writeMethodNotAllowed(...) son precisamente los lugares donde usas el ErrorResponse unificado de la lección anterior, y en el caso de 405 añades además Allow.
Y aquí tienes un ejemplo de writeMethodNotAllowed(...) en su forma mínima. Es corto y «no inteligente», pero hace exactamente lo que necesita el contrato.
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.util.List;
import java.util.Set;
void writeMethodNotAllowed(HttpExchange exchange, Set<String> allowed) throws IOException {
// Pista de transporte para el cliente: qué métodos están permitidos para esta ruta específica
exchange.getResponseHeaders().set("Allow", String.join(", ", allowed));
// Mientras tanto, el cuerpo del error sigue nuestro contrato JSON unificado
writeJson(exchange, 405, new ErrorResponse(
"METHOD_NOT_ALLOWED", "El método no está soportado para esta ruta", List.of()
));
}
Aquí writeJson(...) es tu utilidad ya existente de días anteriores (que establece Content-Type: application/json; charset=utf-8, envía el estado y serializa el body). Si en tu proyecto centralizaste el mapeo de errores, puedes formar ErrorResponse no directamente aquí sino a través de un método de fábrica común, pero la idea es la misma.
7. Verificación de 405 con Postman/curl
Cuando hayas implementado 405, es muy útil hacer una breve verificación manual, porque es uno de esos casos en los que «el código aparentemente compila», pero es fácil confundir el contrato. Lo principal que debes ver: estado 405, encabezado Allow, y error JSON en formato unificado.
Aquí tienes un ejemplo de solicitud que suele ocurrir por error: el cliente intenta hacer POST en una ruta de elemento porque confundió «creación» y «actualización».
# Método incorrecto para el endpoint de elemento
curl -i -X POST http://localhost:8080/api/v1/reading-list/1
# HTTP/1.1 405 Method Not Allowed
# Allow: GET, PUT, DELETE
Si en lugar de esto ves un 404, significa que estás verificando method+path «en un solo montón» y no sabes distinguir una ruta existente de un método incorrecto. Si ves un 400 con un mensaje sobre JSON, significa que estás leyendo el body y validando el DTO antes de verificar si el método está permitido (lo cual, en sentido, es incorrecto).
Aquí tienes otro ejemplo ilustrativo: GET en un endpoint de estado. Existe, pero solo soporta PATCH.
# El endpoint existe, pero GET está prohibido para él
curl -i http://localhost:8080/api/v1/reading-list/1/status
# HTTP/1.1 405 Method Not Allowed
# Allow: PATCH
Y, finalmente, un ejemplo donde debe ser exactamente 404: la ruta no se parece a ninguna de nuestras rutas, ni siquiera estructuralmente.
# La ruta no es reconocida ni estructuralmente — esto es 404
curl -i http://localhost:8080/api/v1/reading-list/1/status/extra
# HTTP/1.1 404 Not Found
Estas tres verificaciones dan una muy buena sensación: no solo «devuelves algo», sino que realmente respondes a la pregunta correcta con el estado correcto.
8. Errores típicos al trabajar con 405 y Allow
Error n.º 1: devolver 404 para una ruta existente con un método no soportado.
Esto suele ocurrir cuando el enrutamiento está escrito al estilo «buscar coincidencia de method+path directamente». Por ejemplo, verificas if (method.equals("GET") && path.equals("/api/v1/reading-list")), y en el else devuelves 404. Como resultado, PUT /api/v1/reading-list se convierte en «ruta no encontrada», aunque la ruta existe. Esto se soluciona no añadiendo más ifs, sino cambiando el orden de pensamiento: primero reconocer la ruta, luego verificar el método.
Error n.º 2: formar Allow como «métodos de toda la aplicación».
Este es un error lógico típico: «bueno, en general tenemos GET/POST/PUT/PATCH/DELETE, así que pongamos todos». Pero Allow debe reflejar solo lo que está permitido para esa ruta. Si le dices al cliente métodos adicionales, no es una pista, es desinformación: intentará PATCH /api/v1/reading-list, obtendrá de nuevo 405 y empezará a dudar de tu cordura (y tendrá razón).
Error n.º 3: leer el body y validar el DTO antes de verificar el método.
Esto no solo es «trabajo extra», sino también una fuente de respuestas incorrectas. Por ejemplo, el cliente hizo un GET con body (sí, a veces ocurre), intentaste parsear el JSON y devolviste un 400 por payload roto, cuando lo correcto habría sido decir 405 (si GET no está soportado para esa ruta) o procesar tranquilamente el GET sin leer el body. En nuestro curso, la regla es simple: la verificación de 404/405 debe ser lo más temprana posible.
Error n.º 4: intentar determinar la existencia de una ruta mediante la «validez de id».
Si decides: «la ruta /reading-list/{id} existe solo si {id} es un número», entonces la solicitud GET /api/v1/reading-list/abc se convertirá en 404, no en 400. Esto rompe la semántica cuidadosa que construimos en el Día 24: un id no numérico es una solicitud incorrecta (400), no «la ruta desapareció». Para esto, el reconocimiento de rutas debe ser estructural (segmentos y partes constantes de la ruta), y las validaciones semánticas deben ser un paso separado.
Error n.º 5: responder a 405 con texto plano mientras todo lo demás es JSON.
Técnicamente, el servidor puede devolver cualquier cosa, pero desde el punto de vista del cliente esto empeora drásticamente la experiencia: debe saber parsear errores en dos formatos. En nuestro proyecto, el objetivo es exactamente el opuesto: todos los errores son ErrorResponse. Por lo tanto, para 405 también devolvemos JSON en una forma unificada, simplemente añadiendo Allow como pista de transporte.
GO TO FULL VERSION