Tratamento de mensagens de erro personalizadas para a API REST
1. Visão geral
Neste tutorial, discutiremos como implementar um manipulador de erros global para uma API Spring REST.
Usaremos a semântica de cada exceção para criar mensagens de erro significativas para o cliente, com o objetivo claro de fornecer a esse cliente todas as informações para diagnosticar facilmente o problema.
Vamos começar implementando uma estrutura simples para enviar erros pela rede - oApiError:
public class ApiError {
private HttpStatus status;
private String message;
private List errors;
public ApiError(HttpStatus status, String message, List errors) {
super();
this.status = status;
this.message = message;
this.errors = errors;
}
public ApiError(HttpStatus status, String message, String error) {
super();
this.status = status;
this.message = message;
errors = Arrays.asList(error);
}
}
As informações aqui devem ser diretas:
status: o código de status HTTP
message: a mensagem de erro associada à exceção
error: Lista de mensagens de erro construídas
E, claro, para a lógica real de tratamento de exceções no Spring,we’ll usea anotação@ControllerAdvice:
@ControllerAdvice
public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler {
...
}
3. Tratar exceções de solicitação incorreta
3.1. Lidando com as exceções
Agora, vamos ver como podemos lidar com os erros mais comuns do cliente - basicamente cenários de um cliente que enviou uma solicitação inválida para a API:
BindException: esta exceção é lançada quando ocorrem erros fatais de ligação.
MethodArgumentNotValidException: Esta exceção é lançada quando o argumento anotado com@Valid falha na validação:
@Override
protected ResponseEntity
Como você pode ver,we are overriding a base method out of the ResponseEntityExceptionHandler and providing our own custom implementation.
Nem sempre será o caso - às vezes precisaremos lidar com uma exceção personalizada que não tem uma implementação padrão na classe base, como veremos mais tarde aqui.
Próximo:
MissingServletRequestPartException: esta exceção é lançada quando a parte de uma solicitação multiparte não é encontrada
MissingServletRequestParameterException: esta exceção é lançada quando a solicitação está faltando o parâmetro:
@Override
protected ResponseEntity
ConstrainViolationException: Esta exceção relata o resultado de violações de restrição:
@ExceptionHandler({ ConstraintViolationException.class })
public ResponseEntity handleConstraintViolation(
ConstraintViolationException ex, WebRequest request) {
List errors = new ArrayList();
for (ConstraintViolation> violation : ex.getConstraintViolations()) {
errors.add(violation.getRootBeanClass().getName() + " " +
violation.getPropertyPath() + ": " + violation.getMessage());
}
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors);
return new ResponseEntity(
apiError, new HttpHeaders(), apiError.getStatus());
}
TypeMismatchException: esta exceção é lançada ao tentar definir a propriedade do bean com o tipo errado.
MethodArgumentTypeMismatchException: esta exceção é lançada quando o argumento do método não é do tipo esperado:
@ExceptionHandler({ MethodArgumentTypeMismatchException.class })
public ResponseEntity handleMethodArgumentTypeMismatch(
MethodArgumentTypeMismatchException ex, WebRequest request) {
String error =
ex.getName() + " should be of type " + ex.getRequiredType().getName();
ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
return new ResponseEntity(
apiError, new HttpHeaders(), apiError.getStatus());
}
3.2. Consumindo a API do cliente
Vamos agora dar uma olhada em um teste que é executado emMethodArgumentTypeMismatchException: vamossend a request with id as String instead of long:
@Test
public void whenMethodArgumentMismatch_thenBadRequest() {
Response response = givenAuth().get(URL_PREFIX + "/api/foos/ccc");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.BAD_REQUEST, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("should be of type"));
}
E finalmente - considerando esse mesmo pedido:
Request method: GET
Request path: http://localhost:8080/spring-security-rest/api/foos/ccc
Esta é a aparência desse tipo de resposta de erro JSON:
{
"status": "BAD_REQUEST",
"message":
"Failed to convert value of type [java.lang.String]
to required type [java.lang.Long]; nested exception
is java.lang.NumberFormatException: For input string: \"ccc\"",
"errors": [
"id should be of type java.lang.Long"
]
}
4. Lidar comNoHandlerFoundException
Em seguida, podemos personalizar nosso servlet para lançar essa exceção em vez de enviar resposta 404 - da seguinte maneira:
api
org.springframework.web.servlet.DispatcherServletthrowExceptionIfNoHandlerFoundtrue
Então, quando isso acontecer, podemos simplesmente lidar com isso como qualquer outra exceção:
@Override
protected ResponseEntity handleNoHandlerFoundException(
NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL();
ApiError apiError = new ApiError(HttpStatus.NOT_FOUND, ex.getLocalizedMessage(), error);
return new ResponseEntity(apiError, new HttpHeaders(), apiError.getStatus());
}
{
"status":"NOT_FOUND",
"message":"No handler found for DELETE /spring-security-rest/api/xx",
"errors":[
"No handler found for DELETE /spring-security-rest/api/xx"
]
}
A seguir, vamos dar uma olhada em outra exceção interessante - oHttpRequestMethodNotSupportedException - que ocorre quando você envia uma solicitação com um método HTTP não suportado:
@Override
protected ResponseEntity handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
StringBuilder builder = new StringBuilder();
builder.append(ex.getMethod());
builder.append(
" method is not supported for this request. Supported methods are ");
ex.getSupportedHttpMethods().forEach(t -> builder.append(t + " "));
ApiError apiError = new ApiError(HttpStatus.METHOD_NOT_ALLOWED,
ex.getLocalizedMessage(), builder.toString());
return new ResponseEntity(
apiError, new HttpHeaders(), apiError.getStatus());
}
Aqui está um teste simples que reproduz esta exceção:
{
"status":"METHOD_NOT_ALLOWED",
"message":"Request method 'DELETE' not supported",
"errors":[
"DELETE method is not supported for this request. Supported methods are GET "
]
}
6. Lidar comHttpMediaTypeNotSupportedException
Agora, vamos lidar comHttpMediaTypeNotSupportedException - que ocorre quando o cliente envia uma solicitação com tipo de mídia não suportado - da seguinte maneira:
@Override
protected ResponseEntity handleHttpMediaTypeNotSupported(
HttpMediaTypeNotSupportedException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
StringBuilder builder = new StringBuilder();
builder.append(ex.getContentType());
builder.append(" media type is not supported. Supported media types are ");
ex.getSupportedMediaTypes().forEach(t -> builder.append(t + ", "));
ApiError apiError = new ApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE,
ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2));
return new ResponseEntity(
apiError, new HttpHeaders(), apiError.getStatus());
}
Aqui está um teste simples para esse problema:
@Test
public void whenSendInvalidHttpMediaType_thenUnsupportedMediaType() {
Response response = givenAuth().body("").post(URL_PREFIX + "/api/foos");
ApiError error = response.as(ApiError.class);
assertEquals(HttpStatus.UNSUPPORTED_MEDIA_TYPE, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("media type is not supported"));
}
Finalmente - aqui está um exemplo de solicitação:
Request method: POST
Request path: http://localhost:8080/spring-security-
Headers: Content-Type=text/plain; charset=ISO-8859-1
Ethe error JSON response:
{
"status":"UNSUPPORTED_MEDIA_TYPE",
"message":"Content type 'text/plain;charset=ISO-8859-1' not supported",
"errors":["text/plain;charset=ISO-8859-1 media type is not supported.
Supported media types are text/xml
application/x-www-form-urlencoded
application/*+xml
application/json;charset=UTF-8
application/*+json;charset=UTF-8 */"
]
}
7. Manipulador padrão
Finalmente, vamos implementar um manipulador de fallback - um tipo de lógica abrangente que lida com todas as outras exceções que não têm manipuladores específicos:
@ExceptionHandler({ Exception.class })
public ResponseEntity handleAll(Exception ex, WebRequest request) {
ApiError apiError = new ApiError(
HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "error occurred");
return new ResponseEntity(
apiError, new HttpHeaders(), apiError.getStatus());
}
8. Conclusão
Construir um manipulador de erro maduro e adequado para uma API REST do Spring é difícil e definitivamente um processo iterativo. Esperamos que este tutorial seja um bom ponto de partida para fazer isso para sua API e também uma boa âncora para como você deve ajudar seus clientes da API a diagnosticar rápida e facilmente os erros e passar por eles.
Ofull implementation deste tutorial pode ser encontrado emthe github project - este é um projeto baseado em Eclipse, portanto, deve ser fácil de importar e executar como está.