From 173f0f79ad854ccb23af32096925c1063227d876 Mon Sep 17 00:00:00 2001 From: Caroline Nordbrandt Date: Tue, 5 May 2026 20:39:00 +0200 Subject: [PATCH 1/6] Implement global exception handling with custom error responses and enhance error management in chat service --- pom.xml | 4 ++ .../exception/ApiErrorResponse.java | 8 +++ .../exception/GlobalExceptionHandler.java | 67 +++++++++++++++++++ .../exception/LLMException.java | 12 ++++ .../projectbifrost/service/ChatService.java | 9 ++- src/main/resources/static/app.js | 35 +++++++--- 6 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java create mode 100644 src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/org/example/projectbifrost/exception/LLMException.java diff --git a/pom.xml b/pom.xml index c589688..2dd870a 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,10 @@ testcontainers-junit-jupiter test + + org.springframework.boot + spring-boot-starter-validation + diff --git a/src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java b/src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java new file mode 100644 index 0000000..205f828 --- /dev/null +++ b/src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java @@ -0,0 +1,8 @@ +package org.example.projectbifrost.exception; + +import java.time.LocalDateTime; + +public record ApiErrorResponse(String message, + int status, + LocalDateTime timestamp) { +} diff --git a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..5215c0e --- /dev/null +++ b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java @@ -0,0 +1,67 @@ +package org.example.projectbifrost.exception; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +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 java.time.LocalDateTime; + +@ControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(LLMException.class) + public ResponseEntity handleLLMException(LLMException ex) { + HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE; // Default 503 + if (ex.getMessage().contains("429") || ex.getMessage().contains("limit")) { + status = HttpStatus.TOO_MANY_REQUESTS; // 429 + } + ApiErrorResponse error = new ApiErrorResponse( + ex.getMessage(), + status.value(), + LocalDateTime.now() + ); + + return new ResponseEntity<>(error, status); + } + + @ExceptionHandler(ResourceAccessException.class) + public ResponseEntity handleTimeoutException(ResourceAccessException ex) { + ApiErrorResponse error = new ApiErrorResponse( + "The connection to the divine realm timed out. Please try again.", + HttpStatus.REQUEST_TIMEOUT.value(), + LocalDateTime.now() + ); + return new ResponseEntity<>(error, HttpStatus.REQUEST_TIMEOUT); + } + + @ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class}) + public ResponseEntity handleBadRequestException(Exception ex) { + ApiErrorResponse error = new ApiErrorResponse( + "The request was invalid: " + ex.getLocalizedMessage(), + HttpStatus.BAD_REQUEST.value(), + LocalDateTime.now() + ); + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + + //Handle other internal exceptions + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneralException(Exception ex) { + // Logg stacktrace + logger.error("Unexpected error: ", ex); + ApiErrorResponse error = new ApiErrorResponse( + "An unexpected error occurred in the Bifrost gateway", + HttpStatus.INTERNAL_SERVER_ERROR.value(), + LocalDateTime.now() + ); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/org/example/projectbifrost/exception/LLMException.java b/src/main/java/org/example/projectbifrost/exception/LLMException.java new file mode 100644 index 0000000..8c2df9d --- /dev/null +++ b/src/main/java/org/example/projectbifrost/exception/LLMException.java @@ -0,0 +1,12 @@ +package org.example.projectbifrost.exception; + +import lombok.Getter; + +@Getter +public class LLMException extends RuntimeException { + private final String provider; + public LLMException(String message, String provider) { + super(message); + this.provider = provider; + } +} diff --git a/src/main/java/org/example/projectbifrost/service/ChatService.java b/src/main/java/org/example/projectbifrost/service/ChatService.java index e9ddd4d..652f4c9 100644 --- a/src/main/java/org/example/projectbifrost/service/ChatService.java +++ b/src/main/java/org/example/projectbifrost/service/ChatService.java @@ -5,8 +5,10 @@ import org.example.projectbifrost.dto.ChatRequestDTO; import org.example.projectbifrost.dto.OpenRouterRequestDTO; import org.example.projectbifrost.dto.OpenRouterResponseDTO; +import org.example.projectbifrost.exception.LLMException; import org.example.projectbifrost.storage.ChatSessionStorage; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; @@ -51,12 +53,15 @@ public String sendRequestToLLM(ChatRequestDTO dto) { OpenRouterResponseDTO result = restClient.post() .uri("/chat/completions")//Start of URI configured in RestClientConfiguration.java - .body(openRouterRequest) //Send JSON-body of messages and model + .body(openRouterRequest) //Send JSON body of messages and model .retrieve() + .onStatus(HttpStatusCode::isError, (request, response) -> { + throw new LLMException("The Gods are silent", model); + }) .body(OpenRouterResponseDTO.class); if (result == null || result.choices() == null || result.choices().isEmpty()) { - throw new RuntimeException("Received empty or invalid response from LLM"); + throw new LLMException("Received empty or invalid response", model); } String content = result.choices().getFirst().message().content(); diff --git a/src/main/resources/static/app.js b/src/main/resources/static/app.js index ec75622..b175aec 100644 --- a/src/main/resources/static/app.js +++ b/src/main/resources/static/app.js @@ -1,8 +1,9 @@ const chatState = { sessionId: crypto.randomUUID(), - isWaiting: false + isWaiting: false //Flag for waiting for response, to prevent multiple sends }; +//Save all HTML-elements to reduce boilerplate const dom = { personality: document.getElementById('personality-select'), input: document.getElementById('user-input'), @@ -16,9 +17,11 @@ const dom = { dom.chips.forEach(chip => { chip.addEventListener('click', () => { + //Remove "active" from all chips dom.chips.forEach(c => c.classList.remove('active')); + //Add "active" to the chip klicked on chip.classList.add('active'); - dom.personality.value = chip.dataset.god; + dom.personality.value = chip.dataset.god; // }); }); @@ -38,9 +41,10 @@ async function sendMessage() { appendMessage('user', null, message); dom.input.value = ''; - setLoading(true); + setLoading(true); //Show text from HTML in waiting for response try { + //AbortController, set a timer for 15 sek while waiting for response const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 15000); let response; @@ -52,27 +56,38 @@ async function sendMessage() { body: JSON.stringify({ personality: selectedPersonality, message: message, - sessionId: chatState.sessionId + sessionId: chatState.sessionId //Randomized id }), signal: controller.signal }); } finally { - clearTimeout(timeoutId); + clearTimeout(timeoutId); //If a response was given, turn of timer } - if (!response.ok) throw new Error("Divine connection lost."); + if (!response.ok) { + try { + const errorData = await response.json(); //Read JSON-error from @ControllerAdvice + throw new Error(errorData.message || 'The Gods are Silent'); + } catch (jsonError) { + //Fallback if not json + throw new Error("Divine connection lost", { cause: jsonError }); + } + } - const aiText = await response.text(); + const aiText = await response.text(); //If ok, read response as text from ai appendMessage('assistant', godName, aiText); } catch (error) { - appendMessage('assistant', 'System', "Error: " + error.message); + appendMessage('assistant', 'System', error.message); } finally { - setLoading(false); + setLoading(false); //Hide loading indicator weather success or not } } +// ── UI helpers ──────────────────────────────────────────────────────── + +//Display and format messages in chat window, with different styling for user and assistant. Also scrolls to bottom when new message is added function appendMessage(role, name, text) { - const msgDiv = document.createElement('div'); + const msgDiv = document.createElement('div');//Create a new HTML element in memory msgDiv.className = `message ${role}`; if (role === 'assistant') { From 56862b879e824c794f32c56759d58c24c4cf3650 Mon Sep 17 00:00:00 2001 From: Caroline Nordbrandt Date: Wed, 6 May 2026 11:24:31 +0200 Subject: [PATCH 2/6] Enhance global exception handling with structured error responses and improved logging; update ApiErrorResponse and LLMException for better error management --- .../exception/ApiErrorResponse.java | 6 +- .../exception/GlobalExceptionHandler.java | 61 +++++++++---------- .../exception/LLMException.java | 9 ++- .../projectbifrost/service/ChatService.java | 9 ++- src/main/resources/static/app.js | 9 +-- 5 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java b/src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java index 205f828..75a28db 100644 --- a/src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java +++ b/src/main/java/org/example/projectbifrost/exception/ApiErrorResponse.java @@ -2,7 +2,9 @@ import java.time.LocalDateTime; -public record ApiErrorResponse(String message, +public record ApiErrorResponse(LocalDateTime timestamp, int status, - LocalDateTime timestamp) { + 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 5215c0e..48f4ddc 100644 --- a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java @@ -1,10 +1,8 @@ package org.example.projectbifrost.exception; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -13,55 +11,52 @@ import java.time.LocalDateTime; @ControllerAdvice +@Slf4j //Structured logging public class GlobalExceptionHandler { - private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); - @ExceptionHandler(LLMException.class) public ResponseEntity handleLLMException(LLMException ex) { - HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE; // Default 503 - if (ex.getMessage().contains("429") || ex.getMessage().contains("limit")) { - status = HttpStatus.TOO_MANY_REQUESTS; // 429 + HttpStatus status; + try { + status = HttpStatus.valueOf(ex.getStatusCode()); //Get the error from OpenRouter + } catch (IllegalArgumentException e) { + status = HttpStatus.INTERNAL_SERVER_ERROR; } - ApiErrorResponse error = new ApiErrorResponse( - ex.getMessage(), - status.value(), - LocalDateTime.now() - ); - return new ResponseEntity<>(error, status); + log.warn("LLM Error [{}]: {}", status.value(), ex.getMessage()); + + return new ResponseEntity<>( + new ApiErrorResponse(LocalDateTime.now(), status.value(), ex.getMessage()), + status + ); } @ExceptionHandler(ResourceAccessException.class) public ResponseEntity handleTimeoutException(ResourceAccessException ex) { - ApiErrorResponse error = new ApiErrorResponse( - "The connection to the divine realm timed out. Please try again.", - HttpStatus.REQUEST_TIMEOUT.value(), - LocalDateTime.now() + 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 ); - return new ResponseEntity<>(error, HttpStatus.REQUEST_TIMEOUT); } - @ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class}) - public ResponseEntity handleBadRequestException(Exception ex) { - ApiErrorResponse error = new ApiErrorResponse( - "The request was invalid: " + ex.getLocalizedMessage(), - HttpStatus.BAD_REQUEST.value(), - LocalDateTime.now() + + @ExceptionHandler(MethodArgumentNotValidException.class) + 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 ); - return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } //Handle other internal exceptions @ExceptionHandler(Exception.class) public ResponseEntity handleGeneralException(Exception ex) { - // Logg stacktrace - logger.error("Unexpected error: ", ex); - ApiErrorResponse error = new ApiErrorResponse( - "An unexpected error occurred in the Bifrost gateway", - HttpStatus.INTERNAL_SERVER_ERROR.value(), - LocalDateTime.now() + log.error("Unexpected error: ", ex); + return new ResponseEntity<>( + new ApiErrorResponse(LocalDateTime.now(), 500, "An unexpected error occurred"), + HttpStatus.INTERNAL_SERVER_ERROR ); - return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); } } diff --git a/src/main/java/org/example/projectbifrost/exception/LLMException.java b/src/main/java/org/example/projectbifrost/exception/LLMException.java index 8c2df9d..03aeee6 100644 --- a/src/main/java/org/example/projectbifrost/exception/LLMException.java +++ b/src/main/java/org/example/projectbifrost/exception/LLMException.java @@ -4,9 +4,12 @@ @Getter public class LLMException extends RuntimeException { - private final String provider; - public LLMException(String message, String provider) { + private final int statusCode; + private final String model; + + public LLMException(String message, String model, int statusCode) { super(message); - this.provider = provider; + this.statusCode = statusCode; + this.model = model; } } diff --git a/src/main/java/org/example/projectbifrost/service/ChatService.java b/src/main/java/org/example/projectbifrost/service/ChatService.java index 652f4c9..a2236e8 100644 --- a/src/main/java/org/example/projectbifrost/service/ChatService.java +++ b/src/main/java/org/example/projectbifrost/service/ChatService.java @@ -8,6 +8,7 @@ import org.example.projectbifrost.exception.LLMException; import org.example.projectbifrost.storage.ChatSessionStorage; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; @@ -56,12 +57,16 @@ public String sendRequestToLLM(ChatRequestDTO dto) { .body(openRouterRequest) //Send JSON body of messages and model .retrieve() .onStatus(HttpStatusCode::isError, (request, response) -> { - throw new LLMException("The Gods are silent", model); + throw new LLMException("The Gods are silent", model, response.getStatusCode().value()); }) .body(OpenRouterResponseDTO.class); if (result == null || result.choices() == null || result.choices().isEmpty()) { - throw new LLMException("Received empty or invalid response", model); + throw new LLMException( + "The Gods sent an empty omen (Invalid response from LLM)", + model, + HttpStatus.BAD_GATEWAY.value() + ); } String content = result.choices().getFirst().message().content(); diff --git a/src/main/resources/static/app.js b/src/main/resources/static/app.js index b175aec..03f040a 100644 --- a/src/main/resources/static/app.js +++ b/src/main/resources/static/app.js @@ -65,13 +65,14 @@ async function sendMessage() { } if (!response.ok) { + let errorMessage = 'The Gods are Silent'; try { const errorData = await response.json(); //Read JSON-error from @ControllerAdvice - throw new Error(errorData.message || 'The Gods are Silent'); - } catch (jsonError) { - //Fallback if not json - throw new Error("Divine connection lost", { cause: jsonError }); + errorMessage = errorData.message || errorMessage; + } catch { + errorMessage = 'Divine connection lost'; } + throw new Error(errorMessage); } const aiText = await response.text(); //If ok, read response as text from ai From 05412f6209b0771318565419170d1a76eb3ed852 Mon Sep 17 00:00:00 2001 From: Caroline Nordbrandt Date: Wed, 6 May 2026 11:58:25 +0200 Subject: [PATCH 3/6] Enhance global exception handling with themed error messages for timeouts and add handling for malformed request bodies --- .../projectbifrost/exception/GlobalExceptionHandler.java | 9 +++++++++ src/main/resources/static/app.js | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java index 48f4ddc..5c80b4f 100644 --- a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java @@ -50,6 +50,15 @@ public ResponseEntity handleValidationException(MethodArgument ); } + @ExceptionHandler(org.springframework.http.converter.HttpMessageNotReadableException.class) + 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 + ); + } + //Handle other internal exceptions @ExceptionHandler(Exception.class) public ResponseEntity handleGeneralException(Exception ex) { diff --git a/src/main/resources/static/app.js b/src/main/resources/static/app.js index 03f040a..71fb9b4 100644 --- a/src/main/resources/static/app.js +++ b/src/main/resources/static/app.js @@ -78,7 +78,11 @@ async function sendMessage() { const aiText = await response.text(); //If ok, read response as text from ai appendMessage('assistant', godName, aiText); } catch (error) { - appendMessage('assistant', 'System', error.message); + //Themed timeout message + const errorMsg = error.name === 'AbortError' + ? 'The Gods took too long to respond...' + : error.message; + appendMessage('assistant', 'System', errorMsg); } finally { setLoading(false); //Hide loading indicator weather success or not } From d95e6b517fc96cb5440f63f7460ca089e0e0d182 Mon Sep 17 00:00:00 2001 From: Caroline Nordbrandt Date: Wed, 6 May 2026 13:01:20 +0200 Subject: [PATCH 4/6] Enhance global exception handling with themed error messages for timeouts and add handling for malformed request bodies --- .../exception/GlobalExceptionHandler.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java index 5c80b4f..7238e10 100644 --- a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java @@ -3,16 +3,18 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; 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; @ControllerAdvice @Slf4j //Structured logging -public class GlobalExceptionHandler { +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(LLMException.class) public ResponseEntity handleLLMException(LLMException ex) { @@ -59,9 +61,24 @@ public ResponseEntity handleUnreadableMessage(org.springframew ); } - //Handle other internal exceptions + /** + * Fallback handler for all unexpected internal exceptions. + * + * This method first checks if the exception implements the ErrorResponse interface (introduced in Spring 6). + * This prevents the handler from masking standard Spring framework errors—such as 404 (Not Found), + * 405 (Method Not Allowed), or 415 (Unsupported Media Type)—with a generic 500 status code. + * If the exception is a genuine, unhandled internal error, it is logged with a full stack trace + * and returns a 500 Internal Server Error. + */ @ExceptionHandler(Exception.class) public ResponseEntity handleGeneralException(Exception ex) { + if (ex instanceof ErrorResponse er) { + HttpStatus status = HttpStatus.valueOf(er.getStatusCode().value()); + return new ResponseEntity<>( + new ApiErrorResponse(LocalDateTime.now(), status.value(), ex.getMessage()), + status + ); + } log.error("Unexpected error: ", ex); return new ResponseEntity<>( new ApiErrorResponse(LocalDateTime.now(), 500, "An unexpected error occurred"), From 6e8f163b36df1720b9d54bf3cebae3823aab524b Mon Sep 17 00:00:00 2001 From: Caroline Nordbrandt Date: Wed, 6 May 2026 20:56:15 +0200 Subject: [PATCH 5/6] Enhance global exception handling by removing ResponseEntityExceptionHandler inheritance and adding validation to chat request DTO --- .../example/projectbifrost/BifrostController.java | 3 ++- .../exception/GlobalExceptionHandler.java | 13 +++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/example/projectbifrost/BifrostController.java b/src/main/java/org/example/projectbifrost/BifrostController.java index f6ac043..15e9319 100644 --- a/src/main/java/org/example/projectbifrost/BifrostController.java +++ b/src/main/java/org/example/projectbifrost/BifrostController.java @@ -1,5 +1,6 @@ package org.example.projectbifrost; +import jakarta.validation.Valid; import org.example.projectbifrost.domain.ChatSession; import org.example.projectbifrost.dto.ChatRequestDTO; import org.example.projectbifrost.service.ChatService; @@ -23,7 +24,7 @@ public String bifrost() { return "Welcome to Bifrost, the gateway to the realms!"; } @PostMapping("/v1/chat") - public String sendChatRequest(@RequestBody ChatRequestDTO dto) { + public String sendChatRequest(@Valid @RequestBody ChatRequestDTO dto) { logger.info("Received chat request: Personality={}, Message={}, SessionId={}", dto.personality(), dto.message(), dto.sessionId()); return chatService.sendRequestToLLM(dto); } diff --git a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java index 7238e10..36fcbfd 100644 --- a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java @@ -14,7 +14,7 @@ @ControllerAdvice @Slf4j //Structured logging -public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { +public class GlobalExceptionHandler { @ExceptionHandler(LLMException.class) public ResponseEntity handleLLMException(LLMException ex) { @@ -61,17 +61,10 @@ public ResponseEntity handleUnreadableMessage(org.springframew ); } - /** - * Fallback handler for all unexpected internal exceptions. - * - * This method first checks if the exception implements the ErrorResponse interface (introduced in Spring 6). - * This prevents the handler from masking standard Spring framework errors—such as 404 (Not Found), - * 405 (Method Not Allowed), or 415 (Unsupported Media Type)—with a generic 500 status code. - * If the exception is a genuine, unhandled internal error, it is logged with a full stack trace - * and returns a 500 Internal Server Error. - */ + @ExceptionHandler(Exception.class) public ResponseEntity handleGeneralException(Exception ex) { + //If Spring has something to say about an error(e.g 404), use it! if (ex instanceof ErrorResponse er) { HttpStatus status = HttpStatus.valueOf(er.getStatusCode().value()); return new ResponseEntity<>( From 267f6829041296606e2e16caecadecfdd851851e Mon Sep 17 00:00:00 2001 From: Caroline Nordbrandt Date: Wed, 6 May 2026 21:28:33 +0200 Subject: [PATCH 6/6] Enhance global exception handling by improving error message handling and adding session ID masking in chat request logging --- .../projectbifrost/BifrostController.java | 16 ++++++++++++---- .../exception/GlobalExceptionHandler.java | 8 ++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/example/projectbifrost/BifrostController.java b/src/main/java/org/example/projectbifrost/BifrostController.java index 15e9319..b643380 100644 --- a/src/main/java/org/example/projectbifrost/BifrostController.java +++ b/src/main/java/org/example/projectbifrost/BifrostController.java @@ -1,17 +1,18 @@ package org.example.projectbifrost; import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; import org.example.projectbifrost.domain.ChatSession; import org.example.projectbifrost.dto.ChatRequestDTO; import org.example.projectbifrost.service.ChatService; import org.slf4j.Logger; import org.springframework.web.bind.annotation.*; +@Slf4j @RestController @RequestMapping("/api") public class BifrostController { - private static final Logger logger = org.slf4j.LoggerFactory.getLogger(BifrostController.class); private final ChatService chatService; @@ -21,16 +22,23 @@ public BifrostController(ChatService chatService) { @GetMapping("/bifrost") public String bifrost() { - return "Welcome to Bifrost, the gateway to the realms!"; } + return "Welcome to Bifrost, the gateway to the realms!"; + } @PostMapping("/v1/chat") public String sendChatRequest(@Valid @RequestBody ChatRequestDTO dto) { - logger.info("Received chat request: Personality={}, Message={}, SessionId={}", dto.personality(), dto.message(), dto.sessionId()); - return chatService.sendRequestToLLM(dto); + log.info("Received chat request: Personality={}, SessionId={}", dto.personality(), maskSessionId(dto.sessionId())); + return chatService.sendRequestToLLM(dto); } @GetMapping("/v1/chat/{sessionId}") public ChatSession getChatHistory(@PathVariable String sessionId) { return chatService.getSessionHistory(sessionId); } + + //Mask when logging seesionId + private String maskSessionId(String sessionId) { + if (sessionId == null || sessionId.length() < 6) return "***"; + return "***" + sessionId.substring(sessionId.length() - 4); + } } diff --git a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java index 36fcbfd..c762af0 100644 --- a/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java +++ b/src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java @@ -66,10 +66,10 @@ public ResponseEntity handleUnreadableMessage(org.springframew public ResponseEntity handleGeneralException(Exception ex) { //If Spring has something to say about an error(e.g 404), use it! if (ex instanceof ErrorResponse er) { - HttpStatus status = HttpStatus.valueOf(er.getStatusCode().value()); - return new ResponseEntity<>( - new ApiErrorResponse(LocalDateTime.now(), status.value(), ex.getMessage()), - status + 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) ); } log.error("Unexpected error: ", ex);