DEV Community

Cover image for Como tratar erros http no Spring Boot
Miguel Barbosa
Miguel Barbosa

Posted on

Como tratar erros http no Spring Boot

Tratamento de erros, independente da linguagem, é sempre uma questão complicada. Mas quando se trata de Spring, existe um padrão recomendado e nativo para lidar com exceções sem muita dor de cabeça.

Este tutorial utiliza Java 17 e Spring boot 3.1.5.
Código completo usado como exemplo.

Sumário

A situação

Temos uma aplicação onde estamos usando a biblioteca de validação padrão do Spring:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

E validamos os campos do DTO da entidade Person:

@Data
public class PersonDto {
  @NotBlank(message = "name: Is required")
  @Length(min = 3, max = 100, message = "title: Must be of 3 - 100 characters")
  String name;

  @NotBlank(message = "email: Is required")
  @Email(message = "email: Invalid format")
  String email;

  @NotNull(message = "age: Is required")
  @Min(value = 1, message = "age: Must be greater than 0")
  @Max(value = 100, message = "age: Must be less than 100")
  Integer age;
}
Enter fullscreen mode Exit fullscreen mode

Após definir nosso DTO, devemos utiliza-lo para representar o body do nosso controller e sempre nos atentarmos na utilização da annotation @Valid para considerar as checagens definidas acima:

@PostMapping
public ResponseEntity<Person> create(@RequestBody @Valid @NotNull PersonDto dto) {
  return ResponseEntity.status(HttpStatus.CREATED).body(personService.create(dto));
}
Enter fullscreen mode Exit fullscreen mode

O problema

Quando enviamos uma requisição para criação de um person com valores inválidos, recebemos algo parecido com isso:

{
  "timestamp": "2023-10-27T00:03:21.577+00:00",
  "status": 400,
  "error": "Bad Request",
  "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<com.m1guelsb.springexceptions.entities.Person> com.m1guelsb.springexceptions.controllers.PersonController.create(com.m1guelsb.springexceptions.dtos.PersonDto) with 3 errors: [Field error in object 'personDto' on field 'email': rejected value [example]; codes [Email.personDto.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [...]"
}
Enter fullscreen mode Exit fullscreen mode

A única informação legível é que o erro foi um 400 Bad request, além disso temos trace e message com valores nem um pouco descritivos. Não sabemos quais foram os campos incorretos, nem quais valores devem ser enviados.

Esse tipo de resposta, além de confundir os usuários de nossa API, também expõem as tecnologias que nosso back-end está usando. Podemos considerar isso como uma brecha de segurança já que toda tecnologia contém falhas.

É importante destacar a parte MethodArgumentNotValidException que indica qual o tipo de erro que estamos recebendo. Precisaremos desta informação adiante.

Filtro global de exceções

O Spring nos provê um jeito nativo para tratar exceções de modo global, o Controller Advice. Podemos usá-lo através da annotation @RestControllerAdvice.

Para isso é ideal criarmos uma classe onde centralizaremos nossos métodos de tratamento de erro e dentro dela teremos um método que irá nos ajudar a padronizar as nossas respostas de erro:

@RestControllerAdvice
public class GlobalExceptionHandler {

  private Map<String, List<String>> errorsMap(List<String> errors) {
    Map<String, List<String>> errorResponse = new HashMap<>();
    errorResponse.put("errors", errors);
    return errorResponse;
  }

}
Enter fullscreen mode Exit fullscreen mode

O método errorsMap vai receber uma lista de String e retornar um Map que terá uma única chave contendo os valores da lista errors. A representação em JSON seria a seguinte:

{
  "errors": [
    //lista de erros
  ]
}
Enter fullscreen mode Exit fullscreen mode

Receptando e tratando erros de validação

Agora finalmente escrevemos o método que irá de fato interceptar os erros e retornar os valores do jeito que queremos:

@RestControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler(MethodArgumentNotValidException.class)
  public ResponseEntity<Map<String, List<String>>> handleValidationErrors(MethodArgumentNotValidException ex) {

    List<String> errors = ex.getBindingResult()
        .getFieldErrors()
        .stream()
        .map(error -> error.getDefaultMessage())
        .toList();

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorsMap(errors));
  }

  private Map<String, List<String>> errorsMap(List<String> errors) {
    Map<String, List<String>> errorResponse = new HashMap<>();
    errorResponse.put("errors", errors);
    return errorResponse;
  }
}
Enter fullscreen mode Exit fullscreen mode

Usando a annotation @ExceptionHandler(), interceptamos as exceções do tipo MethodArgumentNotValidException que é exatamente o mesmo que vimos no trace da resposta anteriormente.

Entendendo o método handleValidationErrors(MethodArgumentNotValidException ex):

  • Recebemos ex como parâmetro do tipo do nosso erro, iteramos em getFieldErrors() e em seguida, coletamos as mensagens de erro usando getDefaultMessage() para retornar uma lista com elas:
List<String> errors = ex.getBindingResult()
    .getFieldErrors()
    .stream()
    .map(error -> error.getDefaultMessage())
    .toList();
Enter fullscreen mode Exit fullscreen mode
  • Então passamos nossa lista de erros para o errorsMap retornando dentro do body do ResponseEntity com o status de BAD_REQUEST:
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorsMap(errors));
Enter fullscreen mode Exit fullscreen mode

Com isso, quando nosso client enviar dados incorretos:

{
  "name": "Mi",
  "email": "Invalid email",
  "age": 0
}
Enter fullscreen mode Exit fullscreen mode

Terá a linda e cheirosa resposta:

{
  "errors": [
    "title: Must be of 3 - 100 characters",
    "age: Must be greater than 0",
    "email: Invalid format"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Meus caros leitores, isso aqui é o sonho de todo dev front-end! 🥰

Tratando outros tipos de erro

Seguindo o mesmo modelo, podemos criar outros métodos que lidarão com outros tipos de erro.

Outro erro muito comum de acontecer é o famoso 404 NOT_FOUND, para trata-lo podemos criar um método estendendo RuntimeException que irá nos ajudar a enviar uma mensagem personalizada para cada caso de NOT_FOUND que tivermos:

public class NotFoundException extends RuntimeException {
  public NotFoundException(String ex) {
    super(ex);
  }
}
Enter fullscreen mode Exit fullscreen mode

E então, no nosso GlobalExceptionHandler adicionamos o seguinte método:

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Map<String, List<String>>> handleNotFoundException(NotFoundException ex) {
  List<String> errors = List.of(ex.getMessage());

  return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorsMap(errors));
}
Enter fullscreen mode Exit fullscreen mode

As únicas diferenças para o método anterior são:

  • Trocamos o parâmetro classe do ExceptionHandler para agora lidar com a nossa classe NotFoundException
  • Agora coletamos a mensagem de erro criando uma lista contendo o seu valor: List.of(ex.getMessage()).

Não podemos esquecer de instanciar e retornar nossa classe sempre que um erro 404 pode ser disparado, como por exemplo o método findById do nosso PersonService:

public Person findById(Long id) throws NotFoundException {
  return personRepository.findById(id)
      .orElseThrow(() -> new NotFoundException("Person with id " + id + " not found"));
}
Enter fullscreen mode Exit fullscreen mode

Dessa forma, quando nosso client tentar acessar um Person que não existe, ele receberá a mensagem que inserimos ao instanciar a classe:

{
  "errors": [
    "Person with id 999 not found"
  ]
}
Enter fullscreen mode Exit fullscreen mode

E por último mas não menos importante, filtramos também os erros gerais dos tipos Exception e RuntimeException:

@ExceptionHandler(Exception.class)
public final ResponseEntity<Map<String, List<String>>> handleGeneralExceptions(Exception ex) {
  List<String> errors = List.of(ex.getMessage());

  return ResponseEntity
      .status(HttpStatus.INTERNAL_SERVER_ERROR)
      .body(errorsMap(errors));
}

@ExceptionHandler(RuntimeException.class)
public final ResponseEntity<Map<String, List<String>>> handleRuntimeExceptions(RuntimeException ex) {
  List<String> errors = List.of(ex.getMessage());

  return ResponseEntity
      .status(HttpStatus.INTERNAL_SERVER_ERROR)
      .body(errorsMap(errors));
}
Enter fullscreen mode Exit fullscreen mode

Seguindo este padrão, podemos tratar qualquer tipo de erro e retornar sempre o mesmo padrão de mensagens. 🥳🎉


Por hoje é isso! Acha que faltou alguma informação importante ou descobriu algum bug? Sinta-se livre para me mandar uma mensagem no Twitter/X.

Obrigado pela leitura! 💝

Top comments (5)

Collapse
 
cherryramatis profile image
Cherry Ramatis

Esse tratamento a nivel de projeto é bem massa, tinha visto no typescript com nestjs e bem provavel que ele tirou dai. Otima didatica e conteudo

Collapse
 
m1guelsb profile image
Miguel Barbosa

Siiim!!! O Nest pegou emprestado muita coisa do Spring 😅

Collapse
 
canhassi profile image
Canhassi

Nice

Collapse
 
clintonrocha98 profile image
Clinton Rocha

Ótimo conteúdo!!

Collapse
 
m1guelsb profile image
Miguel Barbosa

Tamo junto, patrão 🫡