Tilpasset feilmeldingshåndtering for REST API

REST Topp

Jeg kunngjorde nettopp det nye Lær våren kurs, med fokus på det grunnleggende i vår 5 og vårstøvel 2:

>> KONTROLLER KURSET

1. Oversikt

I denne veiledningen vil vi diskutere hvordan du implementerer en global feilbehandler for en Spring REST API.

Vi vil bruke semantikken til hvert unntak for å bygge ut meningsfylte feilmeldinger for klienten, med det klare målet å gi den klienten all informasjonen for å enkelt diagnostisere problemet.

2. En egendefinert feilmelding

La oss starte med å implementere en enkel struktur for å sende feil over ledningen - the ApiError:

offentlig klasse ApiError {privat HttpStatus-status; privat strengmelding; private List-feil; offentlig ApiError (HttpStatus-status, strengmelding, listefeil) {super (); this.status = status; denne meldingen = melding; this.errors = feil; } offentlig ApiError (HttpStatus-status, strengmelding, strengfeil) {super (); this.status = status; denne meldingen = melding; feil = Arrays.asList (feil); }}

Informasjonen her skal være grei:

  • status: HTTP-statuskoden
  • beskjed: feilmeldingen assosiert med unntak
  • feil: Liste over konstruerte feilmeldinger

Og selvfølgelig, for den faktiske unntakshåndteringslogikken våren, bruker vi @ControllerAdvice kommentar:

@ControllerAdvice offentlig klasse CustomRestExceptionHandler utvider ResponseEntityExceptionHandler {...}

3. Håndter unntak fra dårlige forespørsler

3.1. Håndtering av unntakene

La oss nå se hvordan vi kan håndtere de vanligste klientfeilene - i utgangspunktet sendte en klient scenarioer en ugyldig forespørsel til API:

  • BindException: Dette unntaket blir kastet når det oppstår fatale bindingsfeil.
  • MethodArgumentNotValidException: Dette unntaket kastes når argumentet kommenteres med @Gyldig mislykket validering:

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

Som du kan se, vi overstyrer en basemetode ut av ResponseEntityExceptionHandler og tilby vår egen tilpassede implementering.

Det vil ikke alltid være tilfelle - noen ganger trenger vi å håndtere et tilpasset unntak som ikke har en standardimplementering i basisklassen, som vi får se senere her.

Neste:

  • MissingServletRequestPartException: Dette unntaket blir kastet når den delen av en flerdelt forespørsel ikke ble funnet

  • MissingServletRequestParameterException: Dette unntaket kastes når forespørselen mangler parameter:

@ Override-beskyttet ResponseEntity handleMissingServletRequestParameter (MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {Strengfeil = ex.getParameterName () + "parameter mangler"; ApiError apiError = ny ApiError (HttpStatus.BAD_REQUEST, ex.getLocalizedMessage (), feil); returner nye ResponseEntity (apiError, nye HttpHeaders (), apiError.getStatus ()); }
  • ConstrainViolationException: Dette unntaket rapporterer resultatet av tvangsbrudd:

@ExceptionHandler ({ConstraintViolationException.class}) public ResponseEntity handleConstraintViolation (ConstraintViolationException ex, WebRequest request) {List feil = ny ArrayList (); for (ConstraintViolation brudd: ex.getConstraintViolations ()) {feil.add (violation.getRootBeanClass (). getName () + "" + violation.getPropertyPath () + ":" + violation.getMessage ()); } ApiError apiError = ny ApiError (HttpStatus.BAD_REQUEST, ex.getLocalizedMessage (), feil); returner ny ResponseEntity (apiError, nye HttpHeaders (), apiError.getStatus ()); }
  • TypeMismatchException: Dette unntaket kastes når du prøver å angi bønneegenskap med feil type.

  • MethodArgumentTypeMismatchException: Dette unntaket kastes når metodeargument ikke er den forventede typen:

@ExceptionHandler ({MethodArgumentTypeMismatchException.class}) public ResponseEntity handleMethodArgumentTypeMismatch (MethodArgumentTypeMismatchException ex, WebRequest request) {Strengfeil = ex.getName () + "skal være av typen" + ex.getRequiredType (). ApiError apiError = ny ApiError (HttpStatus.BAD_REQUEST, ex.getLocalizedMessage (), feil); returner nye ResponseEntity (apiError, nye HttpHeaders (), apiError.getStatus ()); }

3.2. Forbruker API fra klienten

La oss nå ta en titt på en test som løper inn i en MethodArgumentTypeMismatchException: vi vil send en forespørsel med id som String i stedet for lang:

@Test offentlig ugyldig nårMethodArgumentMismatch_thenBadRequest () {Response response = givenAuth (). Get (URL_PREFIX + "/ api / foos / ccc"); ApiError feil = respons.as (ApiError.class); assertEquals (HttpStatus.BAD_REQUEST, error.getStatus ()); assertEquals (1, error.getErrors (). størrelse ()); assertTrue (error.getErrors (). get (0) .contains ("skal være av typen")); }

Og til slutt - vurderer denne samme forespørselen::

Forespørselsmetode: GET Forespørselssti: // localhost: 8080 / spring-security-rest / api / foos / ccc 

Slik ser denne typen JSON-feilrespons ut:

{"status": "BAD_REQUEST", "message": "Kunne ikke konvertere verdien av typen [java.lang.String] til ønsket type [java.lang.Long]; nestet unntak er java.lang.NumberFormatException: For inngangsstreng : \ "ccc \" "," feil ": [" id skal være av typen java.lang.Long "]}

4. Håndtak NoHandlerFoundException

Deretter kan vi tilpasse servletten vår for å kaste dette unntaket i stedet for å sende 404-svar - som følger:

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

Så når dette skjer, kan vi bare håndtere det akkurat som ethvert annet unntak:

@ Override-beskyttet ResponseEntity handleNoHandlerFoundException (NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {String error = "No handler found for" + ex.getHttpMethod () + "" + ex.getRequestURL (); ApiError apiError = ny ApiError (HttpStatus.NOT_FOUND, ex.getLocalizedMessage (), feil); returner ny ResponseEntity (apiError, nye HttpHeaders (), apiError.getStatus ()); }

Her er en enkel test:

@Test offentlig ugyldig nårNoHandlerForHttpRequest_thenNotFound () {Response response = givenAuth (). Delete (URL_PREFIX + "/ api / xx"); ApiError feil = respons.as (ApiError.class); assertEquals (HttpStatus.NOT_FOUND, error.getStatus ()); assertEquals (1, error.getErrors (). størrelse ()); assertTrue (error.getErrors (). get (0) .contains ("Ingen behandler funnet")); }

La oss ta en titt på den fullstendige forespørselen:

Forespørselsmetode: SLETT Forespørselssti: // localhost: 8080 / spring-security-rest / api / xx

Og feil JSON-svar:

{"status": "NOT_FOUND", "message": "Ingen behandler funnet for DELETE / spring-security-rest / api / xx", "feil": ["Ingen handler funnet for DELETE / spring-security-rest / api / xx "]}

5. Håndtak HttpRequestMethodNotSupportedException

Deretter, la oss ta en titt på et annet interessant unntak - HttpRequestMethodNotSupportedException - som oppstår når du sender en forespurt med en HTTP-metode som ikke støttes:

@ Override-beskyttet ResponseEntity handleHttpRequestMethodNotSupported (HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {StringBuilder builder = new StringBuilder (); builder.append (ex.getMethod ()); builder.append ("metoden støttes ikke for denne forespørselen. Støttede metoder er"); ex.getSupportedHttpMethods (). forEach (t -> builder.append (t + "")); ApiError apiError = ny ApiError (HttpStatus.METHOD_NOT_ALLOWED, ex.getLocalizedMessage (), builder.toString ()); returner ny ResponseEntity (apiError, nye HttpHeaders (), apiError.getStatus ()); }

Her er en enkel test som gjengir dette unntaket:

@Test offentlig ugyldig nårHttpRequestMethodNotSupported_thenMethodNotAllowed () {Response response = givenAuth (). Delete (URL_PREFIX + "/ api / foos / 1"); ApiError feil = respons.as (ApiError.class); assertEquals (HttpStatus.METHOD_NOT_ALLOWED, error.getStatus ()); assertEquals (1, error.getErrors (). størrelse ()); assertTrue (error.getErrors (). get (0) .contains ("Støttede metoder er")); }

Og her er den fulle forespørselen:

Forespørselsmetode: SLETT Forespørselssti: // localhost: 8080 / spring-security-rest / api / foos / 1

Og feil JSON-svar:

{"status": "METHOD_NOT_ALLOWED", "message": "Forespørselsmetode 'DELETE' støttes ikke", "feil": ["SLETT-metoden støttes ikke for denne forespørselen. Støttede metoder er GET"]}

6. Håndtak HttpMediaTypeNotSupportedException

Nå, la oss håndtere HttpMediaTypeNotSupportedException - som oppstår når klienten sender en forespørsel med medietype som ikke støttes - som følger:

@ Override-beskyttet ResponseEntity handleHttpMediaTypeNotSupported (HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {StringBuilder builder = new StringBuilder (); builder.append (ex.getContentType ()); builder.append ("medietype støttes ikke. Støttede medietyper er"); ex.getSupportedMediaTypes (). forEach (t -> builder.append (t + ",")); ApiError apiError = ny ApiError (HttpStatus.UNSUPPORTED_MEDIA_TYPE, ex.getLocalizedMessage (), builder.substring (0, builder.length () - 2)); returner ny ResponseEntity (apiError, nye HttpHeaders (), apiError.getStatus ()); }

Her er en enkel test som går inn i dette problemet:

@Test offentlig ugyldig nårSendInvalidHttpMediaType_thenUnsupportedMediaType () {Response response = givenAuth (). Body (""). Post (URL_PREFIX + "/ api / foos"); ApiError feil = respons.as (ApiError.class); assertEquals (HttpStatus.UNSUPPORTED_MEDIA_TYPE, error.getStatus ()); assertEquals (1, error.getErrors (). størrelse ()); assertTrue (error.getErrors (). get (0) .contains ("medietypen støttes ikke")); }

Til slutt - her er en prøveforespørsel:

Forespørselsmetode: POST Forespørselssti: // localhost: 8080 / spring-security- Overskrifter: Content-Type = text / plain; charset = ISO-8859-1

Og feil JSON-svar:

{"status": "UNSUPPORTED_MEDIA_TYPE", "message": "Innholdstype 'tekst / vanlig; charset = ISO-8859-1' støttes ikke", "feil": ["text / plain; charset = ISO-8859-1 medietype støttes ikke. Støttede medietyper er tekst / xml-applikasjon / x-www-form-urlenkodet applikasjon / * + xml-applikasjon / json; charset = UTF-8-program / * + json; charset = UTF-8 * / " ]}

7. Standard behandler

Til slutt, la oss implementere en tilbakeførselshåndterer - en alt-i-ett-type logikk som omhandler alle andre unntak som ikke har spesifikke håndtere:

@ExceptionHandler ({Exception.class}) public ResponseEntity handleAll (Exception ex, WebRequest request) {ApiError apiError = new ApiError (HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage (), "feil oppstod"); returner nye ResponseEntity (apiError, nye HttpHeaders (), apiError.getStatus ()); }

8. Konklusjon

Å bygge en riktig, moden feilbehandler for en Spring REST API er vanskelig og definitivt en iterativ prosess. Forhåpentligvis vil denne opplæringen være et godt utgangspunkt for å gjøre det for API-en, og også et godt anker for hvordan du skal se på å hjelpe kundene til API-et raskt og enkelt å diagnostisere feil og gå forbi dem.

De full gjennomføring av denne veiledningen finner du i Github-prosjektet - dette er et formørkelsesbasert prosjekt, så det skal være enkelt å importere og kjøre som det er.

HVILLE bunnen

Jeg kunngjorde nettopp det nye Lær våren kurs, med fokus på det grunnleggende i vår 5 og vårstøvel 2:

>> KONTROLLER KURSET

$config[zx-auto] not found$config[zx-overlay] not found