Benutzerdefinierte Fehlernachrichtenbehandlung für die REST-API

Benutzerdefinierte Behandlung von Fehlernachrichten für die REST-API

1. Überblick

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, dem Client alle Informationen zur einfachen Diagnose des Problems bereitzustellen.

Weitere Lektüre:

Spring ResponseStatusException

Erfahren Sie im Frühjahr, wie Sie mit ResponseStatusException Statuscodes auf HTTP-Antworten anwenden.

Read more

Fehlerbehandlung für REST mit Feder

Ausnahmebehandlung für eine REST-API - Veranschaulichen Sie den neuen empfohlenen Spring 3.2-Ansatz sowie frühere Lösungen.

Read more

2. Eine benutzerdefinierte Fehlermeldung

Beginnen wir mit der Implementierung einer einfachen Struktur zum Senden von Fehlern über das Kabel -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

  • error: Liste der erstellten Fehlermeldungen

Und für die eigentliche Ausnahmehandlungslogik im Frühjahr istwe’ll usedie Anmerkung von@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 kommentierte 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,we are overriding a base method out of the ResponseEntityExceptionHandler and providing our own custom implementation.

Dies wird nicht immer der Fall sein. Manchmal müssen wir eine benutzerdefinierte Ausnahme behandeln, für die es keine Standardimplementierung in der Basisklasse gibt, wie wir später hier sehen werden.

Nächster:

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

  • 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 aufMethodArgumentTypeMismatchException stößt: Wir werdensend 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"));
}

Und schließlich - unter Berücksichtigung der gleichen Anfrage:

Request method:  GET
Request path:   http://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. BehandleNoHandlerFoundException

Als nächstes können wir unser Servlet anpassen, um diese Ausnahme auszulösen, anstatt eine 404-Antwort zu senden - wie folgt:


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

Sobald dies passiert ist, 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:   http://localhost:8080/spring-security-rest/api/xx

Und dieerror JSON response:

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

Schauen wir uns als nächstes eine weitere interessante Ausnahme an -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:   http://localhost:8080/spring-security-rest/api/foos/1

Undthe error JSON response:

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

6. BehandleHttpMediaTypeNotSupportedException

Behandeln wir nunHttpMediaTypeNotSupportedException - was auftritt, wenn der Client eine Anforderung mit nicht unterstütztem Medientyp sendet - wie folgt:

@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:   http://localhost:8080/spring-security-
Headers:    Content-Type=text/plain; charset=ISO-8859-1

Undthe 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. Standardhandler

Lassen Sie uns abschließend einen Fallback-Handler implementieren - eine Sammellogik, die alle anderen Ausnahmen behandelt, 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 ausgereiften Fehlerhandlers 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 ein guter Anker dafür, wie Sie den Kunden Ihrer API dabei helfen sollten, Fehler schnell und einfach zu diagnostizieren und an ihnen vorbeizugehen.

Diefull implementation dieses Tutorials finden Sie inthe github project - dies ist ein Eclipse-basiertes Projekt, daher sollte es einfach zu importieren und auszuführen sein.