From bdd800cb0e0e07f6850b115c6ce9517a2602197d Mon Sep 17 00:00:00 2001 From: Caroline Nordbrandt Date: Thu, 7 May 2026 13:34:34 +0200 Subject: [PATCH 1/2] Enhance global exception handling by implementing ProblemDetail responses for various exceptions and enabling detailed error reporting --- .../exception/ApiErrorResponse.java | 10 --- .../exception/GlobalExceptionHandler.java | 67 ++++++++++--------- src/main/resources/application.properties | 1 + 3 files changed, 37 insertions(+), 41 deletions(-) delete mode 100644 src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java diff --git a/src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java b/src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java deleted file mode 100644 index 75a28db..0000000 --- a/src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.example.projectbifrost.exception; - -import java.time.LocalDateTime; - -public record ApiErrorResponse(LocalDateTime timestamp, - int status, - String message) // ← Detailed message for error - -{ -} diff --git a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java index c762af0..995fd59 100644 --- a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java @@ -2,80 +2,85 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; import org.springframework.web.ErrorResponse; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.client.ResourceAccessException; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import java.time.LocalDateTime; +import java.time.Instant; @ControllerAdvice @Slf4j //Structured logging public class GlobalExceptionHandler { + private static final String TIMESTAMP_PROPERTY = "timestamp"; @ExceptionHandler(LLMException.class) - public ResponseEntity handleLLMException(LLMException ex) { + public ResponseEntity handleLLMException(LLMException ex) { HttpStatus status; try { status = HttpStatus.valueOf(ex.getStatusCode()); //Get the error from OpenRouter - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException _) { status = HttpStatus.INTERNAL_SERVER_ERROR; } log.warn("LLM Error [{}]: {}", status.value(), ex.getMessage()); - return new ResponseEntity<>( - new ApiErrorResponse(LocalDateTime.now(), status.value(), ex.getMessage()), - status - ); + ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, ex.getMessage()); + problem.setProperty(TIMESTAMP_PROPERTY, Instant.now().toString()); + problem.setTitle("LLM Error"); + return ResponseEntity.status(status).body(problem); } @ExceptionHandler(ResourceAccessException.class) - public ResponseEntity handleTimeoutException(ResourceAccessException ex) { + public ResponseEntity handleTimeoutException(ResourceAccessException ex) { log.warn("Timeout connecting to LLM: {}", ex.getMessage()); - return new ResponseEntity<>( - new ApiErrorResponse(LocalDateTime.now(), 504, "Connection timeout. The Gods are taking too long to respond."), - HttpStatus.GATEWAY_TIMEOUT - ); + + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.GATEWAY_TIMEOUT, "Connection timeout. The Gods are taking too long to respond."); + problem.setProperty(TIMESTAMP_PROPERTY, Instant.now().toString()); + problem.setTitle("Connection Timeout"); + return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT).body(problem); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) { + public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) { log.warn("Validation error - {} field(s) invalid", ex.getBindingResult().getErrorCount()); - return new ResponseEntity<>( - new ApiErrorResponse(LocalDateTime.now(), 400, "Invalid request - check your input"), - HttpStatus.BAD_REQUEST - ); + + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Invalid request - check your input"); + problem.setProperty(TIMESTAMP_PROPERTY, Instant.now().toString()); + problem.setTitle("Validation Error"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problem); } @ExceptionHandler(org.springframework.http.converter.HttpMessageNotReadableException.class) - public ResponseEntity handleUnreadableMessage(org.springframework.http.converter.HttpMessageNotReadableException ex) { + public ResponseEntity handleUnreadableMessage(org.springframework.http.converter.HttpMessageNotReadableException ex) { log.warn("Malformed request body: {}", ex.getMostSpecificCause().getMessage()); - return new ResponseEntity<>( - new ApiErrorResponse(LocalDateTime.now(), HttpStatus.BAD_REQUEST.value(), "Malformed request body"), - HttpStatus.BAD_REQUEST - ); + + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Malformed request body"); + problem.setProperty(TIMESTAMP_PROPERTY, Instant.now().toString()); + problem.setTitle("Malformed Request"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problem); } @ExceptionHandler(Exception.class) - public ResponseEntity handleGeneralException(Exception ex) { + public ResponseEntity handleGeneralException(Exception ex) { //If Spring has something to say about an error(e.g 404), use it! if (ex instanceof ErrorResponse er) { var status = er.getStatusCode(); String message = ex.getMessage() != null ? ex.getMessage() : "Request failed"; - return ResponseEntity.status(status).body( - new ApiErrorResponse(LocalDateTime.now(), status.value(), message) - ); + ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, message); + problem.setProperty(TIMESTAMP_PROPERTY, Instant.now().toString()); + return ResponseEntity.status(status).body(problem); } log.error("Unexpected error: ", ex); - return new ResponseEntity<>( - new ApiErrorResponse(LocalDateTime.now(), 500, "An unexpected error occurred"), - HttpStatus.INTERNAL_SERVER_ERROR - ); + + ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred"); + problem.setProperty(TIMESTAMP_PROPERTY, Instant.now().toString()); + problem.setTitle("Internal Server Error"); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problem); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b19870c..9fb29b2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,4 @@ spring.application.name=ProjectBifrost +spring.mvc.problemdetails.enabled=true openrouter.api.key=${OPENROUTER_API_KEY} openrouter.model=poolside/laguna-xs.2:free From bed10327b93678ba2736f43a7ef38d7575a40eaa Mon Sep 17 00:00:00 2001 From: Caroline Nordbrandt Date: Thu, 7 May 2026 13:40:49 +0200 Subject: [PATCH 2/2] Enhance global exception handling by adding a title to ProblemDetail responses for request errors --- .../example/projectbifrost/exception/GlobalExceptionHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java index 995fd59..0cc5953 100644 --- a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java @@ -74,6 +74,7 @@ public ResponseEntity handleGeneralException(Exception ex) { String message = ex.getMessage() != null ? ex.getMessage() : "Request failed"; ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, message); problem.setProperty(TIMESTAMP_PROPERTY, Instant.now().toString()); + problem.setTitle("Request Error"); return ResponseEntity.status(status).body(problem); } log.error("Unexpected error: ", ex);