Benutzerdefinierte Behandlung von Fehlermeldungen für die REST-API

REST Top

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs

1. Übersicht

In diesem Tutorial wird erläutert, wie Sie einen globalen Fehlerbehandler für eine Spring REST-API implementieren.

Wir werden die Semantik jeder Ausnahme verwenden, um aussagekräftige Fehlermeldungen für den Client zu erstellen, mit dem klaren Ziel, diesem Client alle Informationen zur einfachen Diagnose des Problems zu geben.

2. Eine benutzerdefinierte Fehlermeldung

Beginnen wir mit der Implementierung einer einfachen Struktur zum Senden von Fehlern über das Kabel - dem ApiError :

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); } }

Die Informationen hier sollten einfach sein:

  • Status : Der HTTP-Statuscode
  • message : Die mit der Ausnahme verbundene Fehlermeldung
  • Fehler : Liste der erstellten Fehlermeldungen

Und natürlich verwenden wir für die eigentliche Logik zur Ausnahmebehandlung im Frühjahr die Annotation @ControllerAdvice :

@ControllerAdvice public class CustomRestExceptionHandler extends ResponseEntityExceptionHandler { ... }

3. Behandeln Sie Ausnahmen für fehlerhafte Anforderungen

3.1. Umgang mit den Ausnahmen

Lassen Sie uns nun sehen, wie wir mit den häufigsten Clientfehlern umgehen können - im Grunde genommen haben Szenarien eines Clients eine ungültige Anforderung an die API gesendet:

  • BindException : Diese Ausnahme wird ausgelöst, wenn schwerwiegende Bindungsfehler auftreten.
  • MethodArgumentNotValidException : Diese Ausnahme wird ausgelöst, wenn das mit @Valid annotierte Argument die Validierung fehlschlägt:

@Override protected ResponseEntity handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { List errors = new ArrayList(); for (FieldError error : ex.getBindingResult().getFieldErrors()) { errors.add(error.getField() + ": " + error.getDefaultMessage()); } for (ObjectError error : ex.getBindingResult().getGlobalErrors()) { errors.add(error.getObjectName() + ": " + error.getDefaultMessage()); } ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), errors); return handleExceptionInternal( ex, apiError, headers, apiError.getStatus(), request); } 

Wie Sie sehen können, überschreiben wir eine Basismethode aus dem ResponseEntityExceptionHandler und stellen unsere eigene benutzerdefinierte Implementierung bereit .

Das wird nicht immer der Fall sein - manchmal müssen wir eine benutzerdefinierte Ausnahme behandeln, die keine Standardimplementierung in der Basisklasse hat, wie wir später hier sehen werden.

Nächster:

  • MissingServletRequestPartException : Diese Ausnahme wird ausgelöst, wenn der Teil einer mehrteiligen Anforderung nicht gefunden wird

  • MissingServletRequestParameterException : Diese Ausnahme wird ausgelöst, wenn der Parameter für die Anforderung fehlt:

@Override protected ResponseEntity handleMissingServletRequestParameter( MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String error = ex.getParameterName() + " parameter is missing"; ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error); return new ResponseEntity( apiError, new HttpHeaders(), apiError.getStatus()); }
  • ConstrainViolationException : Diese Ausnahme meldet das Ergebnis von Einschränkungsverletzungen:

@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 : Diese Ausnahme wird ausgelöst, wenn versucht wird, die Bean-Eigenschaft mit einem falschen Typ festzulegen .

  • MethodArgumentTypeMismatchException : Diese Ausnahme wird ausgelöst, wenn das Methodenargument nicht der erwartete Typ ist:

@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. Konsumieren der API vom Client

Schauen wir uns nun einen Test an, der auf eine MethodArgumentTypeMismatchException stößt : Wir senden eine Anfrage mit der ID als String anstelle von 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")); }

Und schließlich - unter Berücksichtigung derselben Anfrage ::

Request method: GET Request path: //localhost:8080/spring-security-rest/api/foos/ccc 

So sieht diese Art von JSON-Fehlerantwort aus:

{ "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. Behandeln Sie NoHandlerFoundException

Als Nächstes können wir unser Servlet so anpassen, dass diese Ausnahme ausgelöst wird, anstatt eine 404-Antwort zu senden - wie folgt:

 api  org.springframework.web.servlet.DispatcherServlet  throwExceptionIfNoHandlerFound true  

Sobald dies geschieht, können wir es einfach wie jede andere Ausnahme behandeln:

@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()); }

Hier ist ein einfacher Test:

@Test public void whenNoHandlerForHttpRequest_thenNotFound() { Response response = givenAuth().delete(URL_PREFIX + "/api/xx"); ApiError error = response.as(ApiError.class); assertEquals(HttpStatus.NOT_FOUND, error.getStatus()); assertEquals(1, error.getErrors().size()); assertTrue(error.getErrors().get(0).contains("No handler found")); }

Werfen wir einen Blick auf die vollständige Anfrage:

Request method: DELETE Request path: //localhost:8080/spring-security-rest/api/xx

Und die Fehler-JSON-Antwort:

{ "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" ] }

5. Behandeln Sie die HttpRequestMethodNotSupportedException

Schauen wir uns als nächstes eine weitere interessante Ausnahme an - die HttpRequestMethodNotSupportedException - die auftritt, wenn Sie eine Anforderung mit einer nicht unterstützten HTTP-Methode senden:

@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()); }

Hier ist ein einfacher Test, der diese Ausnahme reproduziert:

@Test public void whenHttpRequestMethodNotSupported_thenMethodNotAllowed() { Response response = givenAuth().delete(URL_PREFIX + "/api/foos/1"); ApiError error = response.as(ApiError.class); assertEquals(HttpStatus.METHOD_NOT_ALLOWED, error.getStatus()); assertEquals(1, error.getErrors().size()); assertTrue(error.getErrors().get(0).contains("Supported methods are")); }

Und hier ist die vollständige Anfrage:

Request method: DELETE Request path: //localhost:8080/spring-security-rest/api/foos/1

Und die Fehler-JSON-Antwort:

{ "status":"METHOD_NOT_ALLOWED", "message":"Request method 'DELETE' not supported", "errors":[ "DELETE method is not supported for this request. Supported methods are GET " ] }

6. Behandeln Sie die HttpMediaTypeNotSupportedException

Lassen Sie uns nun die HttpMediaTypeNotSupportedException - die auftritt, wenn der Client eine Anforderung mit nicht unterstütztem Medientyp sendet - wie folgt behandeln:

@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()); }

Hier ist ein einfacher Test, der auf dieses Problem stößt:

@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")); }

Zum Schluss - hier eine Beispielanfrage:

Request method: POST Request path: //localhost:8080/spring-security- Headers: Content-Type=text/plain; charset=ISO-8859-1

Und die Fehler-JSON-Antwort:

{ "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. Standardhandler

Lassen Sie uns abschließend einen Fallback-Handler implementieren - eine Gesamtlogik, die sich mit allen anderen Ausnahmen befasst, für die es keine spezifischen Handler gibt:

@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. Fazit

Das Erstellen eines richtigen, ausgereiften Fehlerbehandlers für eine Spring REST-API ist schwierig und definitiv ein iterativer Prozess. Hoffentlich ist dieses Tutorial ein guter Ausgangspunkt, um dies für Ihre API zu tun, und auch ein guter Anker dafür, wie Sie den Clients Ihrer API helfen sollten, Fehler schnell und einfach zu diagnostizieren und an ihnen vorbeizukommen.

Die vollständige Implementierung dieses Tutorials finden Sie im Github-Projekt - dies ist ein Eclipse-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein.

REST unten

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs