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..0cc5953 100644 --- a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java @@ -2,80 +2,86 @@ 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()); + problem.setTitle("Request Error"); + 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