Custom Exception Class
import org.springframework.http.HttpStatus;
public class BusinessException extends RuntimeException {
private final String errorCode;
private final HttpStatus status;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.status = HttpStatus.INTERNAL_SERVER_ERROR;
}
public BusinessException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.status = HttpStatus.INTERNAL_SERVER_ERROR;
}
public BusinessException(HttpStatus status, String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.status = status;
}
public BusinessException(HttpStatus status, String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.status = status;
}
public HttpStatus getStatus() {
return status;
}
public String getErrorCode() {
return errorCode;
}
}
Error Response DTO
/**
* Immutable error response record with only getter methods
*/
public record ApiResponse(String code, String message) {
}
Global Exception Handler
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.organization.global.constant.SystemErrorCodes;
import com.organization.global.utils.JsonUtils;
import com.organization.global.dto.ApiResponse;
import feign.FeignException;
import feign.RetryableException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse> handleBusinessException(BusinessException e) {
if (e.getCause() != null) {
log.error("Business error occurred: {}", e.getMessage(), e);
}
HttpStatus status = e.getStatus() != null ? e.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
return ResponseEntity.status(status).body(new ApiResponse(e.getErrorCode(), e.getMessage()));
}
@ExceptionHandler(RetryableException.class)
public ResponseEntity<ApiResponse> handleRetryableException(RetryableException e) {
log.error("Retryable error: {}", e.getMessage(), e);
return ResponseEntity.internalServerError()
.body(new ApiResponse(SystemErrorCodes.NETWORK_ERROR, "Network request failed: " + e.getMessage()));
}
@ExceptionHandler(FeignException.class)
public ResponseEntity<ApiResponse> handleFeignException(FeignException e) {
try {
String responseBody = e.contentUTF8();
ApiResponse apiResponse = JsonUtils.parseObject(responseBody, ApiResponse.class);
log.error("Feign client error: {}", e.getMessage(), e);
return ResponseEntity.status(e.status()).body(apiResponse);
} catch (Exception ex) {
log.error("Feign exception parsing failed", ex);
return ResponseEntity.internalServerError()
.body(new ApiResponse(HttpStatus.INTERNAL_SERVER_ERROR.name(), "Service unavailable"));
}
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse> handleValidationException(MethodArgumentNotValidException e) {
FieldError fieldError = e.getBindingResult().getFieldError();
String errorMsg = fieldError != null ? fieldError.getDefaultMessage() : "Invalid request parameters";
return ResponseEntity.badRequest().body(new ApiResponse(HttpStatus.BAD_REQUEST.name(), errorMsg));
}
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<ApiResponse> handleMissingParameterException(MissingServletRequestParameterException e) {
return ResponseEntity.badRequest()
.body(new ApiResponse(HttpStatus.BAD_REQUEST.name(), "Missing required parameter: " + e.getParameterName()));
}
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiResponse> handleMessageNotReadableException(HttpMessageNotReadableException e) {
String errorMessage = "Invalid request format";
Throwable rootCause = e.getCause();
if (rootCause instanceof InvalidFormatException formatException) {
errorMessage += ". Invalid value: " + formatException.getValue();
}
return ResponseEntity.badRequest().body(new ApiResponse(HttpStatus.BAD_REQUEST.name(), errorMessage));
}
@ExceptionHandler(MissingRequestHeaderException.class)
public ResponseEntity<ApiResponse> handleMissingHeaderException(MissingRequestHeaderException e) {
return ResponseEntity.badRequest()
.body(new ApiResponse(HttpStatus.BAD_REQUEST.name(), "Missing required header: " + e.getHeaderName()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse> handleGenericException(Exception e) {
log.error("Unexpected error occurred: {}", e.getMessage(), e);
return ResponseEntity.internalServerError()
.body(new ApiResponse(HttpStatus.INTERNAL_SERVER_ERROR.name(), "Internal server error"));
}
}
Exception Mapping Overview
| Exception Type |
HTTP Status |
Error Code Strategy |
| BusinessException |
Dynamic (configurable) |
Custom error code |
| RetryableException |
500 |
NETWORK_ERROR |
| FeignException |
Original service status |
Preserved from response |
| MethodArgumentNotValidException |
400 |
BAD_REQUEST |
| MissingServletRequestParameterException |
400 |
BAD_REQUEST |
| HttpMessageNotReadableException |
400 |
BAD_REQUEST |
| MissingRequestHeaderException |
400 |
BAD_REQUEST |
| Generic Exception |
500 |
INTERNAL_SERVER_ERROR |