APIs REST: Sobre códigos de retorno

Un buen amigo escribía lo siguiente el otro día en twitter:

Me ha estallado un ojo hoy
1) Llamo a un Endpoint de una API
2) No me devuelve datos porque no encuentra lo que pido
3) Me devuelve un 404

Él estaba en contra del uso de 404 para indicar que no se encuentra un determinado recurso. A partir de aquí se sucedieron varios tweets y eso me ha motivado a escribir este post sobre códigos de retorno en una API REST, con MIS opiniones al respecto, por supuesto! Y como digo siempre, todo debate será bienvenido!

Devolver 404 vs null vs 204

Mucha gente piensa que el 404 significa “la URL no existe”. Pero eso es falso: la URL existe ya que alguien te ha respondido precisamente con un 404. De hecho según la especificación oficial de HTTP (a la que todos deberíamos adherirnos si queremos cumplir REST) significa literalmente “The server has not found anything matching the Request-URI”.

De hecho, como curiosidad, lo más parecido que hay en HTTP a un código que sea “El servicio (NO el recurso) al cual hace referencia la URL no existe” es el 503, pero no significa que no exista si no que está caído (por cualquier razón).

Es decir: el servidor no ha encontrado ningún recurso en esta URL. No dice que la URL sea inválida. También la especificación deja claro que nada en un 404 indica que este sea temporal o permanente. ¿Qué significa eso en el contexto de una API REST? Recuerda: en HTTP (y por lo tanto en REST) las URLs representan recursos. Si un recurso no existe lo suyo es devolver un 404.

¿Crees que a tu cliente le importa si hay un controlador, un middleware, un servlet o lo que sea respondiendo a tu petición? Tu cliente ha pedido un recurso. Si el recurso no existe, no le engañes: devuelvele un 404.

¿Qué no debes hacer nunca de los jamases? Devolver un 200 con un JSON que diga algo como “Recurso no encontrado”. Un 200 significa que la petición es correcta y que en el payload está el resultado. Preguntar por algo que NO existe no es una petición correcta.

¿Qué no debes hacer tampoco nunca? Devolver un “null” serializado. Eso ocurre p. ej. en WebApi. Si devuelves un null, el cliente recibe un 200 con el null serializado (en JSON  es null, pero en XML es un XML feote). A ver… si el cliente te ha pedido la cerveza con id 123, y no existe… ¿por qué le devuelves null? Déjale bien claro que la cerveza con id 123 no existe: 404 al canto.

¿Que otra cosa deberías evitar hacer? Devolver un 204 (No-Content). Un 204 significa que el recurso existe pero está vacío (y por lo tanto no hay nada que mandar al cliente). Es muy distinto que algo exista pero esté vacío a que no exista. Para lo primero 204, para lo segundo 404.

Devolver colecciones vacías ¿sí o no? Imagina que invoca la url /api/beers para pedir todas las cervezas, solo que en este momento no hay ninguna. ¿Qué devuelves? ¿Un 404? ¿O un 200 con un json de array vacío? Para mi esta situación es distinta de la anterior. La pregunta es la misma: ¿existe el recurso /api/beers (la colección de cervezas)? Fíjate que el recurso existe: tenemos una colección de cervezas, solo que ahora está vacía. Así que devolver un 200 con la colección vacía es totalmente correcto, pero si has leído el punto anterior ya habrás deducido que hay otra opción: devolver un 204.

Entre un 204 o un 200 con un array vacío (no contemplo el 404 puesto que no lo considero correcto en este caso) me inclino personalmente con el 200 con el array vacío. Personalmente dejo el 204 para respuestas a peticiones de actualización (síncronas, que si son asíncronas entonces debe ser un 202), en especial los DELETE en los que poca cosa hay que devolver (para creaciones de recursos nuevos es mejor usar el 201). Aunque bueno… los 204 pueden ser peligrosos si quieres usar hypermedia.

Y qué ocurre con los filtros? Como indicamos  que un filtro aplicado produce “ningún resultado”: P. ej. ¿una url tipo “/api/beers?name=e*” que debe devolver si no hay ninguna cerveza cuyo nombre empiece por “e”?

Pues aquí hay cierta confusión. Inicialmente en la RFC2396 la query string no identificaba al recurso. Literalmente pone esto:

The query component is a string of information to be interpreted by the resource

Eso significa que la query string es un dato que debe ser procesado por el recurso. Siguiendo esta definición, entonces el recurso sería /api/beers por lo que devolver un 404 al aplicar query strings sería incorrecto (por el mismo motivo por el cual es incorrecto aplicarlo a la URL sin query string: el recurso sigue existiendo). Pero en la RFC3986 se cambió el significado de la query string:

The query component contains non-hierarchical data that, along with data in the path component (Section 3.3), serves to identify a resource

Eso da a entender que puede usarse la query string para definir el recurso. Bajo este punto de vista, se podría devolver un 200 para /api/beers y un 404 para /api/beers?name=e* ya que serían recursos distintos. Yo personalmente, me inclino por devolver 200 (con el array vacío si no hay resultados) cuando se aplican filtros, pero bueno, ahí queda 🙂

¿401 vs 403?

Esa es otra de las clásicas. Parece que todos tenemos claro cuando devolver un 401: cuando un usuario no autenticado intenta acceder a una URL que es solo para usuarios autenticados. Pero… ¿y si un usuario autenticado intenta acceder a un recurso para el cual él no tiene permisos? ¿Qué le devolvemos en este caso? ¿Un 401? ¿O un 403? Veamos que dice la especifación HTTP al respecto.

Sobre el 401 indica eso (el énfasis es mío):

The request requires user authentication. The response MUST include a WWW-Authenticate header field (section 14.47) containing a challenge applicable to the requested resource. The client MAY repeat the request with a suitable Authorization header field (section 14.8). If the request already included Authorization credentials, then the 401 response indicates that authorization has been refused for those credentials

O sea que según esto es correcto devolver un 401 incluso para usuarios autenticados. ¿Y entonces el 403? Pues la especificación nos dice esto (de nuevo, el énfasis es mío):

The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated

La frase destacada es importante: La autorización no va a funcionar. Es decir, esta petición (por la razón que sea, pero NO tiene que ver con permisos del cliente) está prohibido.

Bajo este punto de vista 403 no debe usarse para indicar que el usuario no tiene permisos y debemos usar un 401… pero resulta que esto NO ES así.

¿Por qué? Pues por el RFC7231. Este RFC define la semántica de la autorización HTTP y, entre otras cosas, da la respuesta a esta cuestión. En su definición del estado 403 pone literalmente:

If authentication credentials were provided in the request, the server considers them insufficient to grant access

O sea que sí. Que se puede devolver un 403 si un usuario autenticado no tiene permisos para acceder a un recurso. Fin de la discusión. Y no solo eso… dado que está permitido usar un 404 en lugar de un 403 si se quiere esconder un recurso no accesible, esto significa que:

  1. Si el usuario no está autenticado o la autenticación es incorrecta debemos devolver un 401
  2. Si el usuario está autenticado pero no tiene permisos podemos devolver o bien un 403 o bien un 404 si queremos “ocultar” la existencia de recursos no accesibles.

Bueno… eso es todo. ¿Y por supuesto… qué opinión tenéis vosotros? ¿Qué otras dudas tenéis en los códigos de retorno?

Saludos!

5 comentarios sobre “APIs REST: Sobre códigos de retorno”

    1. Jajajaa… A mi tampoco, pero la especificación de HTTP lo permite 😉
      Suena un poco a eso de «seguridad por ocultación» pero bueno…

      Gracias!!!

  1. Petición a los reyes magos: Implementación completa de un API REST real world con código fuente (en github, pues codeplex is dead), que siga los good patterns and practices, y sea una referencia para desarrolladores.

  2. Me considero un novato en temas webapi y la primera vez que ví la devolución de un 404 en caso de no encontrar un elemento me pareció cuando menos extraño, muy extraño.
    MIS opiniones iniciales al respecto son que:
    – No somos máquinas, no tenemos por qué responder como máquinas y devolver un 404 está un poco alejado de lo humano.
    – Si lo piensas un poco, es fácil de entender, la página no encuentra lo que tú le estás pidiendo y te dice no lo he encontrado, eso sí en modo máquina jejeje con el 404 pero..
    ¿Cómo diferencio que la página no existe de que no existen resultados? por ejemplo si he escrito mal «beeers/140», espero que me diga no existe LA «PÁGINA» y claro sería lo mismo que si pido «beers/-150» que me diría no existe «LA CERVEZA». Cuando lo has desarrollado tú, es fácil, pero cuando viene de terceros, no sabes si lo estás llamando mal, o si no hay datos, es un poco confuso.

    A medida que vas enredando, te acostumbras y lo ves más usual pero sigo viéndolo como algo «cool» en plan «eh tío, soy humano pero te respondo con códigos de máquina que mola más»

    1. Buenas! Gracias por comentar!
      Un par de comentarios 🙂

      – Nosotros no somos máquinas, pero la mayoría de clientes que consumen las APIs sí lo son: son apps u otros sistemas. En este caso mejor un código claro de error (404) que uno de «todo bien» (200) con un mensaje que ponga «no encontrado». Si tienes que integrarte con una API que siempre devuelve «OK»(200) pero luego en el cuerpo los datos pedidos u otra cosa que puede ser un error, se hace más compleja la integración.
      – El 404 puede tener cuerpo. Es decir puedes devolver un 404 que sea «No se ha encontrado la cerveza» que será distinto de un 404 de página no encontrada. Con lo que, al final, podrías diferenciar estos casos si lo consideras necesario.

      ¡Gracias!

Responder a etomas Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *