GlobalExceptionHandler.java
package com.taxonomy.shared.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
/**
* Global exception handler for all REST controllers.
* Prevents stack traces from leaking to clients and returns
* consistent JSON error responses.
*
* <p>Extends {@link ResponseEntityExceptionHandler} so that Spring MVC binding
* exceptions (e.g. missing required parameters, type mismatches) are correctly
* returned as 4xx responses rather than being caught by the generic 500 handler.
*/
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private final MessageSource messageSource;
public GlobalExceptionHandler(MessageSource messageSource) {
this.messageSource = messageSource;
}
/**
* Handles IllegalArgumentException (bad input from client).
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, Object>> handleBadRequest(IllegalArgumentException ex, WebRequest request) {
log.warn("Bad request: {}", ex.getMessage());
return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), request);
}
/**
* Catch-all handler for any unhandled exception.
* Logs the full stack trace server-side but only returns a safe message to the client.
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGenericException(Exception ex, WebRequest request) {
log.error("Unhandled exception on {}: {}", request.getDescription(false), ex.getMessage(), ex);
Locale locale = LocaleContextHolder.getLocale();
String message = messageSource.getMessage("error.internal", null,
"An internal error occurred. Please try again or check the server logs.", locale);
return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, message, request);
}
/**
* Override the Spring MVC base handler to return our consistent JSON format
* for framework-level exceptions (missing params, type mismatches, etc.).
*/
@Override
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
HttpStatus status = HttpStatus.resolve(statusCode.value());
if (status == null) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
}
if (status.is5xxServerError()) {
log.error("Spring MVC exception on {}: {}", request.getDescription(false), ex.getMessage(), ex);
} else {
log.warn("Spring MVC exception on {}: {}", request.getDescription(false), ex.getMessage());
}
Map<String, Object> errorBody = new LinkedHashMap<>();
errorBody.put("timestamp", Instant.now().toString());
errorBody.put("status", status.value());
errorBody.put("error", status.getReasonPhrase());
errorBody.put("message", ex.getMessage());
errorBody.put("path", request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(status).headers(headers).body(errorBody);
}
private ResponseEntity<Map<String, Object>> buildErrorResponse(HttpStatus status, String message, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", Instant.now().toString());
body.put("status", status.value());
body.put("error", status.getReasonPhrase());
body.put("message", message);
body.put("path", request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(status).body(body);
}
}