Neste artigo, explicarei como retornar erros HTTP personalizados no Spring Boot. Quando fazemos uma solicitação HTTP para um recurso, é comum que a solicitação tenha que considerar a opção de retornar um erro.

É o caso típico em que fizemos uma solicitação RESTful para consultar um registro, mas ele não existe. Nesse caso, você normalmente retornará um código HTTP 404 (Not Found) e, com esse código, também retornará um objeto JSON com um formato definido para Spring Boot, como este:

{
    "timestamp": "2018-11-20T11:46:10.255+0000",
    "status": 404,
    "error": "Not Found",
    "message": "bean: 8 not Found",
    "path": "/get/8"
}

Mas se queremos que a saída seja algo assim:

{
    "timestamp": "2018-11-20T12:51:42.699+0000",
    "mensaje": "bean: 8 not Found",
    "detalles": "uri=/get/8",
    "httpCodeMessage": "Not Found"
}

Temos que colocar uma série de aulas no nosso projeto. Aqui eu explico como.

A partir de um projeto básico Spring Boot, onde temos um objeto simples chamado MiBean com apenas dois campos: code e value. Este objeto será retornado nas solicitações para o recurso /get em um pedido para: http://localhost:8080/get/1 retorne um objeto JSON como este:

{
    "codigo": 1,
    "valor": "valor uno"
}

Se você tentar acessar um elemento com código maior que 3, ele retornará um erro porque apenas três registros estão disponíveis.

Esta é a classe ErrorResource que processa as solicitações para o recurso /get.

public class ErrorResource {

  @Autowired
  MiBeanService service;
  
  @GetMapping("/get/{id}")
  public MiBean getBean(@PathVariable int id) {
    MiBean bean = null;
    try {
       bean = service.getBean(id);
    } catch (NoSuchElementException k) {
      throw new BeanNotFoundException("bean: "+id+ " not Found" );
    }
    return bean;
  }
}

Como visto na função getBean(), chamamos a função getBean(int id) do objeto MiBeanService. Esta é a fonte desse objeto.

@Component
public class MiBeanService {

  private static  List<MiBean> miBeans = new ArrayList<>();
  
  static {
    miBeans.add(new MiBean(1, "valor uno"));
    miBeans.add(new MiBean(2, "valor dos"));
    miBeans.add(new MiBean(3, "valor tres"));
  }
  
  public MiBean getBean(int id) {
    MiBean miBean =
        miBeans.stream()
         .filter(t -> t.getCodigo()==id)
         .findFirst()
         .get();
		 
    return miBean;
  }
}

A função getBean(int id) lança uma exceção do tipo NoSuchElementException se não encontrar o valor em List miBeans. Essa exceção será capturada no controlador e lançará uma exceção do tipo BeanNotFoundException.

A classe BeanNotFoundException é a seguinte:

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

Uma classe simples que estende RuntimeException, e está anotada com a tag @ResponseStatus(HttpStatus.NOT_FOUND) retornará um código 404 para o cliente.

Agora, se fizermos uma solicitação para um código maior que três, receberemos essa resposta:

Mas, como dissemos, queremos que a mensagem de erro seja personalizada.

Para fazer isso, criaremos uma nova classe onde nossos campos definem a mensagem de erro. Esta classe é ExceptionResponse, que é um POJO simples, como você pode ver no código anexado:

public class ExceptionResponse {
  private Date timestamp;
  private String mensaje;
  private String detalles;
  private String httpCodeMessage;
  
  public ExceptionResponse(Date timestamp, String message, String details,String httpCodeMessage) {
    super();
    this.timestamp = timestamp;
    this.mensaje = message;
    this.detalles = details;
    this.httpCodeMessage=httpCodeMessage;
  }
  
  public String getHttpCodeMessage() {
    return httpCodeMessage;
  }
  
  public Date getTimestamp() {
    return timestamp;
  }
  
  public String getMensaje() {
    return mensaje;
  }
  
  public String getDetalles() {
    return detalles;
  }
}

Esta é a classe para indicar qual é o objeto JSON que deve retornar se uma exceção do tipo BeanNotFoundException for lançada.

E agora, escrevemos o código para configurar o Spring para lançar esse objeto JSON.

Este é o write-in: CustomizedResponseEntityExceptionHandler, que está anexado abaixo:

@ControllerAdvice
@RestController
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
  
  @ExceptionHandler(BeanNotFoundException.class)
  public final ResponseEntity<ExceptionResponse> handleNotFoundException(BeanNotFoundException ex, WebRequest request) {
    ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(), ex.getMessage(),
        request.getDescription(false),HttpStatus.NOT_ACCEPTABLE.getReasonPhrase());

    return new ResponseEntity<ExceptionResponse>(exceptionResponse, HttpStatus.NOT_ACCEPTABLE);
  }
}

Essa classe deve estender o ResponseEntityExceptionHandler, que manipula as exceções mais comuns.

Deve ser anotado com os anotações @ControllerAdvice e @RestController.

@ControllerAdvice é derivado de @Component e será usado para classes que lidam com exceções. E como a classe tem o rótulo @RestContoller, ele manipula apenas as exceções lançadas nos controladores REST.

Na função handleNotFoundException, definimos que quando a exceção BeanNotFoundException é lançada, ela deve retornar um objeto ExceptionResponse. Isso é feito criando um objeto ResponseEntity convenientemente iniciado.

É importante observar que ele define o código HTTP retornado. Neste caso, retornamos o código 406 em vez do 404. De fato, em nosso exemplo, poderíamos remover o rótulo @ResponseStatus(HttpStatus.NOT_FOUND) para a classe BeanNotFoundException e tudo funcionaria da mesma forma.

E assim, temos uma saída personalizada, conforme mostrado na imagem a seguir: