Cómo devolver códigos de estado HTTP en una aplicación Spring Boot

En esta guía, veremos cómo devolver códigos de estado HTTP en aplicaciones Spring Boot con @ResponseStatus, ResponseEntity y ResponseStatusException.

Introducción

A todos los ingenieros de software que confían en servicios o herramientas externos/de terceros a través de HTTP les gustaría saber si sus solicitudes han sido aceptadas y, en caso contrario, qué está pasando.

Su papel como desarrollador de API es proporcionar una buena experiencia para sus usuarios y, entre otras cosas, satisfacer esta demanda. Hacer que sea fácil para otros desarrolladores determinar si su API devuelve un error o no lo lleva muy lejos, y en el primer caso, dejar que otros desarrolladores sepan por qué lo lleva aún más lejos.

¿El error es causado por un servicio interno de la API? ¿Enviaron un valor no analizable? ¿Se bloqueó el servidor que procesaba estas solicitudes?

Reducir las posibilidades de falla permite que los desarrolladores que usan su servicio hagan su trabajo de manera más eficiente. Aquí es donde entran en juego los códigos de estado HTTP, con un mensaje corto en el cuerpo de la respuesta, que describe lo que está pasando.

En esta guía, veremos cómo devolver diferentes códigos de estado HTTP en Spring Boot, mientras desarrollamos una API REST.

¿Qué son los códigos de estado HTTP?

En pocas palabras, un código de estado HTTP se refiere a un código de 3 dígitos que forma parte de la respuesta HTTP de un servidor. El primer dígito del código describe la categoría en la que cae la respuesta. Esto ya da una pista para determinar si la solicitud fue exitosa o no. La Autoridad de Números Asignados en Internet (IANA) mantiene el registro oficial de Códigos de Estado HTTP. A continuación se muestran las diferentes categorías:

  1. Informativo (1xx): Indica que se recibió la solicitud y el proceso continúa. Alerta al remitente para que espere una respuesta final.
  2. Successful (2xx): indica que la solicitud se recibió, entendió y aceptó correctamente.
  3. Redireccionamiento (3xx): indica que se deben realizar más acciones para completar la solicitud.
  4. Errores de Cliente (4xx): Indica que ocurrió un error durante el procesamiento de la solicitud y es el cliente quien provocó el error.
  5. Errores del servidor (5xx): Indica que ocurrió un error durante el procesamiento de la solicitud pero que fue por parte del servidor.

Si bien la lista no es exhaustiva, estos son algunos de los códigos HTTP más comunes con los que se encontrará:


Código Estado Descripción 200 OK La solicitud se completó con éxito. 201 Creado Se creó correctamente un nuevo recurso. 400 Solicitud incorrecta La solicitud no era válida. 401 No autorizado La solicitud no incluía un token de autenticación o el token de autenticación expiró. 403 Prohibido El cliente no tenía permiso para acceder al recurso solicitado. 404 No encontrado No se encontró el recurso solicitado. 405 Método no permitido El método HTTP en la solicitud no fue compatible con el recurso. Por ejemplo, el método DELETE no se puede utilizar con la API del agente. 500 Error interno del servidor La solicitud no se completó debido a un error interno en el lado del servidor. 503 Servicio no disponible El servidor no estaba disponible.


Devolver códigos de estado HTTP en Spring Boot

Spring Boot hace que el desarrollo de aplicaciones basadas en Spring sea mucho más fácil que nunca y devuelve automáticamente los códigos de estado apropiados. Si la solicitud se realizó correctamente, se devuelve un 200 OK, mientras que se devuelve un 404 Not Found si el recurso no se encuentra en el servidor.

Sin embargo, hay muchas situaciones en las que nos gustaría decidir sobre el código de estado HTTP que se devolverá en la respuesta y Spring Boot nos ofrece varias formas de lograrlo.

Comencemos un proyecto de esqueleto a través de Spring Initializr:

O a través de Spring CLI:

1
$ spring init -d=web

Tendremos un controlador simple, TestController:

1
2
@Controller
public class TestController {}

Aquí, crearemos algunos controladores de solicitudes que devuelvan diferentes códigos de estado, a través de algunos enfoques diferentes.

Devolución de códigos de estado de respuesta con @ResponseStatus

Esta anotación toma como argumento el código de estado HTTP que se devolverá en la respuesta. Spring facilita nuestro trabajo al proporcionar una enumeración que contiene todos los códigos de estado HTTP. Es una anotación muy versátil y se puede usar en controladores a nivel de clase o de método, en clases de excepción personalizadas y en clases anotadas con @ControllerAdvice (a nivel de clase o de método).

Funciona de la misma manera en ambas clases anotadas con @ControllerAdvice y aquellas anotadas con @Controller. Por lo general, se combina con la anotación @ResponseBody en ambos casos. Cuando se usa a nivel de clase, todos los métodos de clase darán como resultado una respuesta con el código de estado HTTP especificado. Todas las anotaciones de @ResponseStatus a nivel de método anulan el código de nivel de clase y si no hay @ResponseStatus asociado con un método que no genera una excepción, se devuelve un 200 de forma predeterminada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Controller
@ResponseBody
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public class TestController {
    
    @GetMapping("/classlevel")
    public String serviceUnavailable() {
        return "The HTTP Status will be SERVICE_UNAVAILABLE (CODE 503)\n";
    }

    @GetMapping("/methodlevel")
    @ResponseStatus(code = HttpStatus.OK, reason = "OK")
    public String ok() {
        return "Class Level HTTP Status Overriden. The HTTP Status will be OK (CODE 200)\n";
    }    
}

El @ResponseStatus de nivel de clase se convierte en el código predeterminado que se devolverá para todos los métodos, a menos que un método lo anule. El controlador de solicitudes /classlevel no está asociado con un estado de nivel de método, por lo que el estado de nivel de clase se activa y devuelve un 503 Servicio no disponible si alguien llega al punto final. Por otro lado, el extremo /methodlevel devuelve un 200 OK:

1
2
3
4
5
6
7
8
9
$ curl -i 'http://localhost:8080/classlevel'

HTTP/1.1 503
Content-Type: text/plain;charset=UTF-8
Content-Length: 55
Date: Thu, 17 Jun 2021 06:37:37 GMT
Connection: close

The HTTP Status will be SERVICE_UNAVAILABLE (CODE 503)
1
2
3
4
5
6
7
8
$ curl -i 'http://localhost:8080/methodlevel'

HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 73
Date: Thu, 17 Jun 2021 06:41:08 GMT

Class Level HTTP Status Overriden. The HTTP Status will be OK (CODE 200)

@ResponseStatus funciona de manera diferente cuando se usa en clases de excepciones personalizadas. Aquí, el código de estado HTTP especificado será el que se devuelva en la respuesta cuando se produzca una excepción de ese tipo pero no se detecte. Echaremos un vistazo más de cerca a todo esto en el código en una sección posterior.

Además, puede especificar un motivo, que activa automáticamente el método HttpServletResponse.sendError(), lo que significa que cualquier cosa que devuelva no sucederá:

1
2
3
4
5
    @GetMapping("/methodlevel")
    @ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Resource was not found on the server")
    public String notFound() {
        return "";
    }

Sin embargo, para obtener el motivo para enviarlo a través del método sendError(), tendrá que configurar la propiedad include-message dentro de application.properties:

1
server.error.include-message=always

Ahora, si enviamos una solicitud a /methodlevel:

1
2
3
4
5
6
7
$ curl -i http://localhost:8080/methodlevel
HTTP/1.1 404
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 29 Jun 2021 16:52:28 GMT

{"timestamp":"2021-06-29T16:52:28.894+00:00","status":404,"error":"Not Found","message":"Resource was not found on the server","path":"/methodlevel"}

Esta es probablemente la forma más sencilla de devolver un estado HTTP, pero también rígido. Realmente no podemos alterar los códigos de estado manualmente, a través del código aquí. Aquí es donde entra en juego la clase ResponseEntity.

Devolución de códigos de estado de respuesta con ResponseEntity

La clase ResponseEntity se usa cuando especificamos mediante programación todos los aspectos de una respuesta HTTP. Esto incluye los encabezados, el cuerpo y, por supuesto, el código de estado. Esta es la forma más detallada de devolver una respuesta HTTP en Spring Boot, pero también la más personalizable. Muchos prefieren usar la anotación @ResponseBody junto con @ResponseStatus ya que son más simples. Se puede crear un objeto ResponseEntity utilizando uno de los varios constructores o mediante el método de construcción estático:

1
2
3
4
5
6
7
8
9
@Controller
@ResponseBody
public class TestController {
    
    @GetMapping("/response_entity")
    public ResponseEntity<String> withResponseEntity() {
        return ResponseEntity.status(HttpStatus.CREATED).body("HTTP Status will be CREATED (CODE 201)\n");
    }   
}

La principal ventaja de usar una ResponseEntity es que puede vincularla con otra lógica, como:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Controller
@ResponseBody
public class TestController {
    
    @GetMapping("/response_entity")
    public ResponseEntity<String> withResponseEntity() {
        int randomInt = new Random().ints(1, 1, 11).findFirst().getAsInt();
        if (randomInt < 9) {
            return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body("Expectation Failed from Client (CODE 417)\n");   
        } else {
            return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).body("April Fool's Status Code (CODE 418)\n");
        }
    }   
}

Aquí, hemos generado un entero aleatorio dentro de un rango de 1 y 10, y devuelto un código de estado según el entero aleatorio. Al verificar si randomInt es mayor que 9, le hemos dado al cliente un 10% de probabilidad de ver el código de estado de "I am a teapot" April Fool, agregado a RFC2324.

Enviar varias solicitudes a este punto final eventualmente devolverá:

1
2
3
4
5
6
7
8
$ curl -i 'http://localhost:8080/response_entity'

HTTP/1.1 418
Content-Type: text/plain;charset=UTF-8
Content-Length: 36
Date: Tue, 29 Jun 2021 16:36:21 GMT

April Fool's Status Code (CODE 418)

Devolución de códigos de estado de respuesta con ResponseStatusException

Una clase utilizada para devolver códigos de estado en casos excepcionales es la clase ResponseStatusException. Se utiliza para devolver un mensaje específico y el código de estado HTTP que se devolverá cuando se produzca un error. Es una alternativa al uso de @ExceptionHandler y @ControllerAdvice. El manejo de excepciones usando ResponseStatusException se considera más detallado. Evita la creación de clases de excepción adicionales innecesarias y reduce el estrecho acoplamiento entre los códigos de estado y las propias clases de excepción:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Controller
@ResponseBody
public class TestController {

    @GetMapping("/rse")
    public String withResponseStatusException() {
        try {
            throw new RuntimeException("Error Occurred");
        } catch (RuntimeException e) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "HTTP Status will be NOT FOUND (CODE 404)\n");
        }
    }   
}

Se comporta de manera muy parecida a cuando establecemos el motivo a través de un @ResponseStatus, ya que el mecanismo subyacente es el mismo: el método sendError():

1
2
3
4
5
6
7
$ curl -i http://localhost:8080/rse
HTTP/1.1 404
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 29 Jun 2021 17:00:23 GMT

{"timestamp":"2021-06-29T17:01:17.874+00:00","status":404,"error":"Not Found","message":"HTTP Status will be NOT FOUND (CODE 404)\n","path":"/rse"}

Clases de excepción personalizadas y devolución de códigos de estado HTTP

Finalmente, otra forma de manejar las excepciones es a través de las anotaciones @ResponseStatus y @ControllerAdvice y las clases de excepción personalizadas. Aunque se prefiere ResponseStatusException, si por alguna razón no está en la imagen, siempre puede usar estos.

Agreguemos dos controladores de solicitudes que lanzan nuevas excepciones personalizadas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Controller
@ResponseBody
@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
public class TestController {

    @GetMapping("/caught")
    public String caughtException() {
        throw new CaughtCustomException("Caught Exception Thrown\n");
    }

    @GetMapping("/uncaught")
    public String unCaughtException() {
        throw new UnCaughtException("The HTTP Status will be BAD REQUEST (CODE 400)\n");
    }

}

Ahora, definamos estas excepciones y sus propios códigos @ResponseStatus predeterminados (que anulan el estado de nivel de clase):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class CaughtCustomException extends RuntimeException{
    public CaughtCustomException(String message) {
        super(message);
    }
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class UnCaughtException extends RuntimeException {
    public UnCaughtException(String message) {
        super(message);
    }
}

Finalmente, crearemos un controlador @ControllerAdvice, que se usa para configurar cómo Spring Boot administra las excepciones:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@ControllerAdvice
@ResponseBody
public class TestControllerAdvice {

    @ExceptionHandler(CaughtCustomException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String handleException(CaughtCustomException exception) {
        return String.format("The HTTP Status will be Internal Server Error (CODE 500)\n %s\n",exception.getMessage()) ;
    }
}

Finalmente, cuando activamos algunas solicitudes HTTP, el punto final que devuelve CaughCustomException se formateará de acuerdo con @ControllerAdvice, mientras que UnCaughtCustomException no lo hará:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ curl -i http://localhost:8080/caught
HTTP/1.1 500
Content-Type: text/plain;charset=UTF-8
Content-Length: 83
Date: Tue, 29 Jun 2021 17:10:01 GMT
Connection: close

The HTTP Status will be Internal Server Error (CODE 500)
 Caught Exception Thrown


$ curl -i http://localhost:8080/uncaught
HTTP/1.1 400
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 29 Jun 2021 17:10:06 GMT
Connection: close

{"timestamp":"2021-06-29T17:10:06.264+00:00","status":400,"error":"Bad Request","message":"The HTTP Status will be BAD REQUEST (CODE 400)\n","path":"/uncaught"}

Conclusión

En esta guía, hemos analizado cómo devolver códigos de estado HTTP en Spring Boot usando @ResponseStatus, ResponseEntity y ResponseStatusException, así como también cómo definir excepciones personalizadas y manejarlas a través de @ ControllerAdvice y sin él.