From 5e9e16a273cf7ed6661c565c371c3d6a6090617d Mon Sep 17 00:00:00 2001 From: Alexander Dudkin Date: Sun, 25 Aug 2024 22:59:30 +0300 Subject: [PATCH 1/2] feat: add timestamp field for exception handling --- .../petclinic/rest/advice/ExceptionControllerAdvice.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java b/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java index a01b1ebc8..f5c856f70 100644 --- a/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java +++ b/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java @@ -31,6 +31,8 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.context.request.WebRequest; +import java.time.ZonedDateTime; + import static org.springframework.http.HttpStatus.BAD_REQUEST; /** @@ -53,9 +55,9 @@ public class ExceptionControllerAdvice { * @param className The name of the exception class * @param exMessage The message of the exception */ - private record ErrorInfo(String className, String exMessage) { + private record ErrorInfo(String className, String exMessage, ZonedDateTime timestamp) { public ErrorInfo(Exception ex) { - this(ex.getClass().getName(), ex.getLocalizedMessage()); + this(ex.getClass().getName(), ex.getLocalizedMessage(), ZonedDateTime.now()); } } @@ -101,7 +103,7 @@ public ResponseEntity handleMethodArgumentNotValidException(MethodArg BindingResult bindingResult = ex.getBindingResult(); if (bindingResult.hasErrors()) { errors.addAllErrors(bindingResult); - return ResponseEntity.badRequest().body(new ErrorInfo("MethodArgumentNotValidException", "Validation failed")); + return ResponseEntity.badRequest().body(new ErrorInfo("MethodArgumentNotValidException", "Validation failed", ZonedDateTime.now())); } return ResponseEntity.badRequest().build(); } From 81ea3a1991adfe2aee6e10671821b7bc760bf6c2 Mon Sep 17 00:00:00 2001 From: Alexander Dudkin Date: Wed, 4 Sep 2024 18:35:29 +0300 Subject: [PATCH 2/2] refactor: switch to RFC 9457 Problem Details in ControllerAdvice --- .../advice/ExceptionControllerAdvice.java | 136 +++++++++++------- 1 file changed, 82 insertions(+), 54 deletions(-) diff --git a/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java b/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java index f5c856f70..641a4cb34 100644 --- a/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java +++ b/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java @@ -16,14 +16,12 @@ package org.springframework.samples.petclinic.rest.advice; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; -import org.springframework.samples.petclinic.rest.controller.BindingErrorsResponse; -import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -31,81 +29,111 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.context.request.WebRequest; -import java.time.ZonedDateTime; +import java.net.URI; +import java.time.OffsetDateTime; +import java.util.stream.Collectors; -import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static java.time.OffsetDateTime.now; +import static org.springframework.http.HttpStatus.*; /** - * Global Exception handler for REST controllers. - *

- * This class handles exceptions thrown by REST controllers and returns - * appropriate HTTP responses to the client. - * - * @author Vitaliy Fedoriv * @author Alexander Dudkin */ @ControllerAdvice public class ExceptionControllerAdvice { /** - * Record for storing error information. - *

- * This record encapsulates the class name and message of the exception. + * Handles RuntimeException and returns a {@link ProblemDetail} with HTTP 500 status. * - * @param className The name of the exception class - * @param exMessage The message of the exception + * @param ex the thrown RuntimeException + * @param request the current WebRequest + * @return a ResponseEntity with a ProblemDetail and HTTP 500 status */ - private record ErrorInfo(String className, String exMessage, ZonedDateTime timestamp) { - public ErrorInfo(Exception ex) { - this(ex.getClass().getName(), ex.getLocalizedMessage(), ZonedDateTime.now()); - } + @ExceptionHandler(RuntimeException.class) + @ResponseStatus(code = INTERNAL_SERVER_ERROR) + @ResponseBody + public ResponseEntity handleRuntimeException(RuntimeException ex, WebRequest request) { + return createProblemDetail( + "Unexpected error", + INTERNAL_SERVER_ERROR.value(), + ex.getLocalizedMessage(), + request, + now() + ); } /** - * Handles all general exceptions by returning a 500 Internal Server Error status with error details. + * Handles DataIntegrityViolationException and returns a {@link ProblemDetail} with HTTP 404 status. * - * @param e The exception to be handled - * @return A {@link ResponseEntity} containing the error information and a 500 Internal Server Error status + * @param ex the thrown DataIntegrityViolationException + * @param request the current WebRequest + * @return a ResponseEntity with a ProblemDetail and HTTP 404 status */ - @ExceptionHandler(Exception.class) - public ResponseEntity handleGeneralException(Exception e) { - ErrorInfo info = new ErrorInfo(e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(info); + @ExceptionHandler(DataIntegrityViolationException.class) + @ResponseStatus(code = NOT_FOUND) + @ResponseBody + public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex, WebRequest request) { + return createProblemDetail( + "Data Integrity Violation", + NOT_FOUND.value(), + ex.getLocalizedMessage(), + request, + now() + ); } /** - * Handles {@link DataIntegrityViolationException} which typically indicates database constraint violations. - * This method returns a 404 Not Found status if an entity does not exist. + * Handles MethodArgumentNotValidException and returns a {@link ProblemDetail} with HTTP 400 status. + * Aggregates all validation error messages into a single detail string. * - * @param ex The {@link DataIntegrityViolationException} to be handled - * @return A {@link ResponseEntity} containing the error information and a 404 Not Found status + * @param ex the thrown MethodArgumentNotValidException + * @param request the current WebRequest + * @return a ResponseEntity with a ProblemDetail and HTTP 400 status */ - @ExceptionHandler(DataIntegrityViolationException.class) - @ResponseStatus(code = HttpStatus.NOT_FOUND) - @ResponseBody - public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex) { - ErrorInfo errorInfo = new ErrorInfo(ex); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorInfo); + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, WebRequest request) { + String detail = ex.getBindingResult().getAllErrors().stream() + .map(this::formatErrorMessage) + .collect(Collectors.joining("; ")); + + return createProblemDetail( + "Validation Error", + BAD_REQUEST.value(), + detail, + request, + now() + ); } /** - * Handles exception thrown by Bean Validation on controller methods parameters + * Formats the error message from an ObjectError. * - * @param ex The thrown exception - * - * @return an empty response entity + * @param error the ObjectError to format + * @return a formatted error message */ - @ExceptionHandler(MethodArgumentNotValidException.class) - @ResponseStatus(BAD_REQUEST) - @ResponseBody - public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { - BindingErrorsResponse errors = new BindingErrorsResponse(); - BindingResult bindingResult = ex.getBindingResult(); - if (bindingResult.hasErrors()) { - errors.addAllErrors(bindingResult); - return ResponseEntity.badRequest().body(new ErrorInfo("MethodArgumentNotValidException", "Validation failed", ZonedDateTime.now())); - } - return ResponseEntity.badRequest().build(); + private String formatErrorMessage(ObjectError error) { + return (error instanceof FieldError fieldError) + ? String.format("Field '%s' %s", fieldError.getField(), fieldError.getDefaultMessage()) + : error.getDefaultMessage(); } + /** + * Creates a ProblemDetail object with the provided details. + * + * @param title the title of the problem + * @param status the HTTP status code + * @param detail the detail message of the problem + * @param request the current WebRequest + * @param timestamp the timestamp of the problem occurrence + * @return a ResponseEntity with the ProblemDetail and the specified status + */ + private ResponseEntity createProblemDetail(String title, int status, String detail, WebRequest request, OffsetDateTime timestamp) { + ProblemDetail problemDetail = ProblemDetail.forStatus(status); + problemDetail.setTitle(title); + problemDetail.setDetail(detail); + problemDetail.setInstance(URI.create(request.getDescription(false).substring(4))); + problemDetail.setProperty("timestamp", timestamp); + + return new ResponseEntity<>(problemDetail, HttpStatus.valueOf(status)); + } }