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
- O problema
- Filtro global de exceções
- Receptando e tratando erros de validação
- Tratando outros tipos de erro
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>
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;
}
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));
}
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 [...]"
}
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;
}
}
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
]
}
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;
}
}
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 emgetFieldErrors()
e em seguida, coletamos as mensagens de erro usandogetDefaultMessage()
para retornar uma lista com elas:
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getDefaultMessage())
.toList();
- Então passamos nossa lista de erros para o
errorsMap
retornando dentro dobody
doResponseEntity
com o status deBAD_REQUEST
:
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorsMap(errors));
Com isso, quando nosso client enviar dados incorretos:
{
"name": "Mi",
"email": "Invalid email",
"age": 0
}
Terá a linda e cheirosa resposta:
{
"errors": [
"title: Must be of 3 - 100 characters",
"age: Must be greater than 0",
"email: Invalid format"
]
}
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);
}
}
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));
}
As únicas diferenças para o método anterior são:
- Trocamos o parâmetro classe do
ExceptionHandler
para agora lidar com a nossa classeNotFoundException
- 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"));
}
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"
]
}
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));
}
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)
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
Siiim!!! O Nest pegou emprestado muita coisa do Spring 😅
Nice
Ótimo conteúdo!!
Tamo junto, patrão 🫡