MS Lesson 17: Exception Handling


1. Ilk once eger bizim proyektde Global Exception Handler olmadigi ucun program exception atsa Spring onu 500 Internal Server Error kimi qaytarir.

Bu tipli response aliriq:

{

  "timestamp": "2026-05-14T17:17:04.482Z",

  "status": 500,

  "error": "Internal Server Error",

  "path": "/find-by-id/1"

}


2. Hetta bu terzde yazsaq bele bu problemi hell etmir:

@Transactional
public Account foo(Long id) {
return accountRepository.findById(id).orElseThrow(() -> new RuntimeException("Xeta"));
}


3. Bu problemi hell etmek ucun, sade model quraq. 

package guru.springframework.cruddemo.error;

import java.time.Instant;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {

private Integer status;
private String code;
private String message;
private Instant timestamp;
private String path;

}

response model:

{

  "status": 404,

  "code": "ACCOUNT_NOT_FOUND",

  "message": "Account was not found",

  "timestamp": "2026-05-14T10:15:30Z"

}



Novbeti addim ise BaseException model qura bilerik.

package guru.springframework.cruddemo.error;

import java.util.Arrays;

import org.springframework.http.HttpStatus;

import lombok.Getter;

@Getter
public class BaseException extends RuntimeException {

private final ErrorCode errorCode;
private final Object[] detailsParams;

public BaseException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.detailsParams = new Object[0];
}

public BaseException(ErrorCode errorCode, Object... detailsParams) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.detailsParams = copyParams(detailsParams);
}

public BaseException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.detailsParams = new Object[0];
}

public BaseException(ErrorCode errorCode, Throwable cause, Object... detailsParams) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.detailsParams = copyParams(detailsParams);
}

private static Object[] copyParams(Object... detailsParams) {
if (detailsParams == null || detailsParams.length == 0) {
return new Object[0];
}
return Arrays.copyOf(detailsParams, detailsParams.length);
}

public String getFormattedDetails() {
return errorCode.formatDetails(detailsParams);
}

public HttpStatus getHttpStatus() {
return errorCode.getHttpStatus();
}
}



package guru.springframework.cruddemo.error;

import java.text.MessageFormat;
import java.util.Arrays;

import org.springframework.http.HttpStatus;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ErrorCode {

VALIDATION_FAILED(
"VALIDATION_FAILED",
"Validation failed",
"One or more fields failed validation.",
HttpStatus.BAD_REQUEST),
METHOD_VALIDATION_FAILED(
"METHOD_VALIDATION_FAILED",
"Method validation failed",
"One or more method parameters failed validation.",
HttpStatus.BAD_REQUEST),
CONSTRAINT_VIOLATION(
"CONSTRAINT_VIOLATION",
"Constraint violation",
"One or more constraints were violated.",
HttpStatus.BAD_REQUEST),
REQUEST_BODY_NOT_READABLE(
"REQUEST_BODY_NOT_READABLE",
"Malformed request body",
"Request body is missing or malformed.",
HttpStatus.BAD_REQUEST),
MISSING_REQUEST_PARAMETER(
"MISSING_REQUEST_PARAMETER",
"Missing request parameter",
"A required request parameter is missing.",
HttpStatus.BAD_REQUEST),
TYPE_MISMATCH(
"TYPE_MISMATCH",
"Type mismatch",
"Request parameter type is invalid.",
HttpStatus.BAD_REQUEST),
BAD_REQUEST(
"BAD_REQUEST",
"Bad request",
"Request is malformed or contains invalid data.",
HttpStatus.BAD_REQUEST),
AKB_INTEGRATION_ERROR(
"AKB_INTEGRATION_ERROR",
"AKB integration error",
"AKB integration failed during {0}.",
HttpStatus.SERVICE_UNAVAILABLE),
INTERNAL_SERVER_ERROR(
"INTERNAL_SERVER_ERROR",
"Internal server error",
"An unexpected error occurred.",
HttpStatus.INTERNAL_SERVER_ERROR),
SUBMISSION_ALREADY_EXISTS(
"IDEMPOTENCY_KEY_EXISTS",
"Submission with this idempotency key already exists",
"A credit submission with idempotency key ''{0}'' has already been processed. This is a duplicate request. If you need to submit a new credit, please generate a new idempotency key.",
HttpStatus.CONFLICT),
SUBMISSION_NOT_FOUND(
"SUBMISSION_NOT_FOUND",
"Submission not found",
"Submission with ID ''{0}'' was not found.",
HttpStatus.NOT_FOUND),
SCORING_INQUIRY_NOT_FOUND(
"SCORING_INQUIRY_NOT_FOUND",
"Scoring inquiry not found",
"Scoring inquiry with ID ''{0}'' was not found.",
HttpStatus.NOT_FOUND),
ACCOUNT_NOT_FOUND(
"ACCOUNT_NOT_FOUND",
"Account not found",
"Account with ID ''{0}'' was not found.",
HttpStatus.NOT_FOUND),

UNKNOWN(
"UNKNOWN",
"Unknown error",
"An unrecognized error code was received.",
HttpStatus.INTERNAL_SERVER_ERROR);

@Getter(onMethod_ = @JsonValue)
private final String code;
private final String message;
private final String detailsTemplate;
private final HttpStatus httpStatus;

public String formatDetails(Object... params) {
if (params == null || params.length == 0) {
return this.detailsTemplate;
}
return MessageFormat.format(this.detailsTemplate, params);
}

@JsonCreator
public static ErrorCode fromCode(String value) {
if (value == null || value.isBlank()) {
return null;
}
return Arrays.stream(values())
.filter(c -> c.code.equals(value))
.findFirst()
.orElse(UNKNOWN);
}
}


Indi ise exceptionu spesifikleshdire bilerik:

package guru.springframework.cruddemo.error;

public class AccountNotFoundException extends BaseException {

public AccountNotFoundException(Long accountId) {
super(ErrorCode.ACCOUNT_NOT_FOUND, accountId);
}

}


Novbeti addim ise GlobalExceptionHandler yazmaq lazimdir:

package guru.springframework.cruddemo.error;

import java.time.Instant;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BaseException.class)
public ResponseEntity<ErrorResponse> handleBaseException(BaseException ex, WebRequest request) {
ErrorResponse body = ErrorResponse.builder()
.status(ex.getHttpStatus().value())
.code(ex.getErrorCode().getCode())
.message(ex.getFormattedDetails())
.timestamp(Instant.now())
.path(((ServletWebRequest) request).getRequest().getRequestURI())
.build();
return ResponseEntity.status(ex.getHttpStatus()).body(body);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex, WebRequest request) {
log.error("Unhandled exception", ex);
ErrorCode code = ErrorCode.INTERNAL_SERVER_ERROR;
ErrorResponse body = ErrorResponse.builder()
.status(code.getHttpStatus().value())
.code(code.getCode())
.message(code.getMessage())
.timestamp(Instant.now())
.path(((ServletWebRequest) request).getRequest().getRequestURI())
.build();
return ResponseEntity.status(code.getHttpStatus()).body(body);
}

}




Indi ise field-ler ucun error model quraq. 

package guru.springframework.cruddemo.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import guru.springframework.cruddemo.dto.CreateAccountRequest;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequestMapping("/validation-demo")
public class ValidationDemoController {

@PostMapping("/accounts")
public ResponseEntity<String> createAccount(@Valid @RequestBody CreateAccountRequest request) {
log.info("Valid request: name={}, email={}, balance={}",
request.getName(), request.getEmail(), request.getBalance());
return ResponseEntity.ok("Validation passed");
}

}


package guru.springframework.cruddemo.dto;

import java.math.BigDecimal;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;

@Data
public class CreateAccountRequest {

@NotBlank(message = "name must not be blank")
private String name;

@NotBlank(message = "email must not be blank")
@Email(message = "email must be valid")
private String email;

@NotNull(message = "balance must not be null")
@Positive(message = "balance must be greater than zero")
private BigDecimal balance;

}


Field error tutulmasa response bu shekilde gelecek:

{

  "status": 500,

  "code": "INTERNAL_SERVER_ERROR",

  "message": "Internal server error",

  "timestamp": "2026-05-16T07:23:22.083954Z",

  "path": "/validation-demo"

}



Bunun ucun ayrica error response model qururuq:

package guru.springframework.cruddemo.error;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class FieldErrorDto {

private String field;
private String message;

}


package guru.springframework.cruddemo.error;

import java.time.Instant;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {

private Integer status;
private String code;
private String message;
private Instant timestamp;
private String path;
private List<FieldErrorDto> fieldErrors;

}


package guru.springframework.cruddemo.error;

import java.time.Instant;
import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(BaseException.class)
public ResponseEntity<ErrorResponse> handleBaseException(BaseException ex, WebRequest request) {
ErrorResponse body = ErrorResponse.builder()
.status(ex.getHttpStatus().value())
.code(ex.getErrorCode().getCode())
.message(ex.getFormattedDetails())
.timestamp(Instant.now())
.path(requestPath(request))
.build();
return ResponseEntity.status(ex.getHttpStatus()).body(body);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
WebRequest request) {
ErrorCode code = ErrorCode.VALIDATION_FAILED;
List<FieldErrorDto> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
.map(fieldError -> FieldErrorDto.builder()
.field(fieldError.getField())
.message(fieldError.getDefaultMessage())
.build())
.toList();
ErrorResponse body = ErrorResponse.builder()
.status(code.getHttpStatus().value())
.code(code.getCode())
.message(code.getMessage())
.timestamp(Instant.now())
.path(requestPath(request))
.fieldErrors(fieldErrors)
.build();
return ResponseEntity.status(code.getHttpStatus()).body(body);
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ErrorResponse> handleHttpMessageNotReadable(
HttpMessageNotReadableException ex,
WebRequest request) {
ErrorCode code = ErrorCode.REQUEST_BODY_NOT_READABLE;
ErrorResponse body = ErrorResponse.builder()
.status(code.getHttpStatus().value())
.code(code.getCode())
.message(code.getMessage())
.timestamp(Instant.now())
.path(requestPath(request))
.build();
return ResponseEntity.status(code.getHttpStatus()).body(body);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex, WebRequest request) {
log.error("Unhandled exception", ex);
ErrorCode code = ErrorCode.INTERNAL_SERVER_ERROR;
ErrorResponse body = ErrorResponse.builder()
.status(code.getHttpStatus().value())
.code(code.getCode())
.message(code.getMessage())
.timestamp(Instant.now())
.path(requestPath(request))
.build();
return ResponseEntity.status(code.getHttpStatus()).body(body);
}

private static String requestPath(WebRequest request) {
if (request instanceof ServletWebRequest servletWebRequest) {
return servletWebRequest.getRequest().getRequestURI();
}
return request.getDescription(false);
}

}











==========================================================================




package com.azercell.akbintegrationservice.error.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

import java.text.MessageFormat;

@Getter
@RequiredArgsConstructor
public enum ErrorCode {

VALIDATION_FAILED(
"VALIDATION_FAILED",
"Validation failed",
"One or more fields failed validation.",
HttpStatus.BAD_REQUEST
),
METHOD_VALIDATION_FAILED(
"METHOD_VALIDATION_FAILED",
"Method validation failed",
"One or more method parameters failed validation.",
HttpStatus.BAD_REQUEST
),
CONSTRAINT_VIOLATION(
"CONSTRAINT_VIOLATION",
"Constraint violation",
"One or more constraints were violated.",
HttpStatus.BAD_REQUEST
),
REQUEST_BODY_NOT_READABLE(
"REQUEST_BODY_NOT_READABLE",
"Malformed request body",
"Request body is missing or malformed.",
HttpStatus.BAD_REQUEST
),
MISSING_REQUEST_PARAMETER(
"MISSING_REQUEST_PARAMETER",
"Missing request parameter",
"A required request parameter is missing.",
HttpStatus.BAD_REQUEST
),
TYPE_MISMATCH(
"TYPE_MISMATCH",
"Type mismatch",
"Request parameter type is invalid.",
HttpStatus.BAD_REQUEST
),
BAD_REQUEST(
"BAD_REQUEST",
"Bad request",
"Request is malformed or contains invalid data.",
HttpStatus.BAD_REQUEST
),
AKB_INTEGRATION_ERROR(
"AKB_INTEGRATION_ERROR",
"AKB integration error",
"AKB integration failed during {0}.",
HttpStatus.SERVICE_UNAVAILABLE
),
INTERNAL_SERVER_ERROR(
"INTERNAL_SERVER_ERROR",
"Internal server error",
"An unexpected error occurred.",
HttpStatus.INTERNAL_SERVER_ERROR
),
SUBMISSION_ALREADY_EXISTS(
"IDEMPOTENCY_KEY_EXISTS",
"Submission with this idempotency key already exists",
"A credit submission with idempotency key ''{0}'' has already been processed. This is a duplicate request. If you need to submit a new credit, please generate a new idempotency key.",
HttpStatus.CONFLICT
),
SUBMISSION_NOT_FOUND(
"SUBMISSION_NOT_FOUND",
"Submission not found",
"Submission with ID ''{0}'' was not found.",
HttpStatus.NOT_FOUND
),
SCORING_INQUIRY_NOT_FOUND(
"SCORING_INQUIRY_NOT_FOUND",
"Scoring inquiry not found",
"Scoring inquiry with ID ''{0}'' was not found.",
HttpStatus.NOT_FOUND
),
;

private final String code;
private final String message;
private final String detailsTemplate;
private final HttpStatus httpStatus;

public String formatDetails(Object... params) {
if (params == null || params.length == 0) {
return this.detailsTemplate;
}
return MessageFormat.format(this.detailsTemplate, params);
}

} 


package com.azercell.akbintegrationservice.error.model;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.Instant;
import java.util.List;
import java.util.Map;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorResponse {

private String traceId;
private Integer status;
private String reason;
private String code;
private String message;
private String details;
private String path;
private Instant timestamp;
private List<ValidationError> validationErrors;
private Map<String, Object> additionalProperties;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ValidationError {

private String field;
private Object value;
private String message;
private String code;

}

}



package com.azercell.akbintegrationservice.error.model;

import com.azercell.akbintegrationservice.error.enums.ErrorCode;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ExternalServiceErrorResponse {

private String timestamp;
private Integer status;
private String error;
private String message;
private String path;
private ErrorCode errorCode;

}



package com.azercell.akbintegrationservice.error.handler;

import com.azercell.akbintegrationservice.error.enums.ErrorCode;
import com.azercell.akbintegrationservice.error.exceptions.AkbIntegrationException;
import com.azercell.akbintegrationservice.error.exceptions.ExternalServiceException;
import com.azercell.akbintegrationservice.error.exceptions.IdempotencyConflictException;
import com.azercell.akbintegrationservice.error.exceptions.ScoringInquiryNotFoundException;
import com.azercell.akbintegrationservice.error.exceptions.SubmissionNotFoundException;
import com.azercell.akbintegrationservice.error.model.ErrorResponse;
import com.azercell.akbintegrationservice.error.model.ExternalServiceErrorResponse;
import com.azercell.akbintegrationservice.logging.filter.RequestIdFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.Nullable;
import org.slf4j.MDC;
import org.springframework.beans.TypeMismatchException;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.validation.method.MethodValidationException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

@Override
protected @Nullable ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
String traceId = resolveTraceId();

List<ErrorResponse.ValidationError> validationErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(this::mapFieldError)
.collect(Collectors.toList());

ErrorCode errorCode = ErrorCode.VALIDATION_FAILED;
String path = resolvePath(request);

log.warn("Validation failed - traceId: {}, errors: {}, path: {}", traceId, validationErrors.size(), path);

return ResponseEntity
.status(errorCode.getHttpStatus())
.body(buildErrorResponse(errorCode, traceId, path, validationErrors));
}

@Override
protected @Nullable ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
ErrorCode errorCode = ErrorCode.REQUEST_BODY_NOT_READABLE;
String traceId = resolveTraceId();
String path = resolvePath(request);

log.warn("Malformed request body - traceId: {}, path: {}", traceId, path);

return ResponseEntity
.status(errorCode.getHttpStatus())
.body(buildErrorResponse(errorCode, traceId, path));
}

@Override
protected @Nullable ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request
) {
ErrorCode errorCode = ErrorCode.MISSING_REQUEST_PARAMETER;
String traceId = resolveTraceId();
String path = resolvePath(request);

ErrorResponse.ValidationError validationError = ErrorResponse.ValidationError.builder()
.field(ex.getParameterName())
.message(ex.getMessage())
.code("MissingServletRequestParameter")
.build();

log.warn("Missing request parameter - traceId: {}, path: {}", traceId, path);

return ResponseEntity
.status(errorCode.getHttpStatus())
.body(buildErrorResponse(errorCode, traceId, path, List.of(validationError)));
}

@Override
protected @Nullable ResponseEntity<Object> handleMethodValidationException(MethodValidationException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
ErrorCode errorCode = ErrorCode.METHOD_VALIDATION_FAILED;
String traceId = resolveTraceId();
String path = resolvePath(request);

List<ErrorResponse.ValidationError> validationErrors = ex.getAllErrors()
.stream()
.map(this::mapResolvableError)
.collect(Collectors.toList());

log.warn("Method validation failed - traceId: {}, errors: {}, path: {}", traceId, validationErrors.size(), path);

return ResponseEntity
.status(errorCode.getHttpStatus())
.body(buildErrorResponse(errorCode, traceId, path, validationErrors));
}

@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleConstraintViolationException(
ConstraintViolationException ex,
HttpServletRequest request
) {
ErrorCode errorCode = ErrorCode.CONSTRAINT_VIOLATION;
String traceId = resolveTraceId();
String path = request.getRequestURI();

List<ErrorResponse.ValidationError> validationErrors = ex.getConstraintViolations()
.stream()
.map(this::mapConstraintViolation)
.collect(Collectors.toList());

log.warn("Constraint violations - traceId: {}, errors: {}, path: {}", traceId, validationErrors.size(), path);

return ResponseEntity
.status(errorCode.getHttpStatus())
.body(buildErrorResponse(errorCode, traceId, path, validationErrors));
}

@Override
protected @Nullable ResponseEntity<Object> handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
ErrorCode errorCode = ErrorCode.TYPE_MISMATCH;
String traceId = resolveTraceId();
String path = resolvePath(request);

ErrorResponse.ValidationError validationError = ErrorResponse.ValidationError.builder()
.field(ex.getPropertyName())
.value(ex.getValue())
.message(ex.getMessage())
.code("TypeMismatch")
.build();

log.warn("Type mismatch - traceId: {}, path: {}", traceId, path);

return ResponseEntity
.status(errorCode.getHttpStatus())
.body(buildErrorResponse(errorCode, traceId, path, List.of(validationError)));
}

@Override
protected @Nullable ResponseEntity<Object> handleExceptionInternal(Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR;
String traceId = resolveTraceId();
String path = resolvePath(request);

log.error("Unhandled exception - traceId: {}, path: {}", traceId, path, ex);

return ResponseEntity
.status(statusCode)
.body(buildErrorResponse(errorCode, traceId, path));
}

@ExceptionHandler(ExternalServiceException.class)
public ResponseEntity<Object> handleExternalServiceException(ExternalServiceException ex,
WebRequest request) {
ExternalServiceErrorResponse externalError = ex.getExternalServiceError();
ErrorCode errorCode = ex.getErrorCode();

String traceId = resolveTraceId();
String path = externalError != null && externalError.getPath() != null
? externalError.getPath()
: resolvePath(request);

String detailsOverride = externalError != null && externalError.getMessage() != null
? externalError.getMessage()
: errorCode.formatDetails();
int status = externalError != null && externalError.getStatus() != null
? externalError.getStatus()
: errorCode.getHttpStatus().value();
String reason = externalError != null && externalError.getError() != null
? externalError.getError()
: errorCode.getHttpStatus().getReasonPhrase();
Map<String, Object> additionalProperties = externalError != null
? Map.of("externalServiceError", externalError)
: Map.of();

log.warn("External service error - traceId: {}, path: {}", traceId, path, ex);

return ResponseEntity
.status(status)
.body(buildErrorResponseWithStatus(
errorCode,
traceId,
path,
detailsOverride,
List.of(),
additionalProperties,
status,
reason
));
}

@ExceptionHandler(AkbIntegrationException.class)
public ResponseEntity<Object> handleAkbIntegrationException(AkbIntegrationException ex,
WebRequest request) {
ErrorCode errorCode = ErrorCode.AKB_INTEGRATION_ERROR;
String traceId = resolveTraceId();
String path = resolvePath(request);

log.warn("AKB integration error - traceId: {}, path: {}", traceId, path, ex);

return ResponseEntity
.status(errorCode.getHttpStatus())
.body(buildErrorResponse(
errorCode,
traceId,
path,
ex.getFormattedDetails(),
null,
ex.getAdditionalProperties()
));
}

@ExceptionHandler(IdempotencyConflictException.class)
public ResponseEntity<Object> handleIdempotencyConflict(IdempotencyConflictException ex,
WebRequest request) {
ErrorCode errorCode = ErrorCode.SUBMISSION_ALREADY_EXISTS;
String traceId = resolveTraceId();
String path = resolvePath(request);

log.warn("Idempotency conflict - traceId: {}, path: {}", traceId, path, ex);

return ResponseEntity
.status(errorCode.getHttpStatus())
.body(buildErrorResponse(
errorCode,
traceId,
path,
ex.getFormattedDetails(),
null,
ex.getAdditionalProperties()
));
}

@ExceptionHandler(SubmissionNotFoundException.class)
public ResponseEntity<Object> handleSubmissionNotFound(SubmissionNotFoundException ex,
WebRequest request) {
ErrorCode errorCode = ErrorCode.SUBMISSION_NOT_FOUND;
String traceId = resolveTraceId();
String path = resolvePath(request);

log.warn("Submission not found - traceId: {}, path: {}", traceId, path, ex);

return ResponseEntity
.status(errorCode.getHttpStatus())
.body(buildErrorResponse(
errorCode,
traceId,
path,
ex.getFormattedDetails(),
null,
ex.getAdditionalProperties()
));
}

@ExceptionHandler(ScoringInquiryNotFoundException.class)
public ResponseEntity<Object> handleScoringInquiryNotFound(ScoringInquiryNotFoundException ex,
WebRequest request) {
ErrorCode errorCode = ErrorCode.SCORING_INQUIRY_NOT_FOUND;
String traceId = resolveTraceId();
String path = resolvePath(request);

log.warn("Scoring inquiry not found - traceId: {}, path: {}", traceId, path, ex);

return ResponseEntity
.status(errorCode.getHttpStatus())
.body(buildErrorResponse(
errorCode,
traceId,
path,
ex.getFormattedDetails(),
null,
ex.getAdditionalProperties()
));
}

private ErrorResponse.ValidationError mapFieldError(FieldError fieldError) {
return ErrorResponse.ValidationError.builder()
.field(fieldError.getField())
.value(fieldError.getRejectedValue())
.message(fieldError.getDefaultMessage())
.code(fieldError.getCode())
.build();
}

private ErrorResponse.ValidationError mapObjectError(ObjectError error) {
if (error instanceof FieldError fieldError) {
return mapFieldError(fieldError);
}

return ErrorResponse.ValidationError.builder()
.field(error.getObjectName())
.message(error.getDefaultMessage())
.code(error.getCode())
.build();
}

private ErrorResponse.ValidationError mapResolvableError(MessageSourceResolvable resolvable) {
if (resolvable instanceof ObjectError objectError) {
return mapObjectError(objectError);
}

String message = resolvable.getDefaultMessage();
String[] codes = resolvable.getCodes();
String code = codes != null && codes.length > 0
? codes[0]
: null;

return ErrorResponse.ValidationError.builder()
.message(message)
.code(code)
.build();
}

private ErrorResponse.ValidationError mapConstraintViolation(ConstraintViolation<?> violation) {
String code = violation.getConstraintDescriptor() != null
&& violation.getConstraintDescriptor().getAnnotation() != null
? violation.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName()
: null;

return ErrorResponse.ValidationError.builder()
.field(violation.getPropertyPath() != null ? violation.getPropertyPath().toString() : null)
.value(violation.getInvalidValue())
.message(violation.getMessage())
.code(code)
.build();
}

private ErrorResponse buildErrorResponse(
ErrorCode errorCode,
String traceId,
String path
) {
return buildErrorResponse(errorCode, traceId, path, null, null, null);
}

private ErrorResponse buildErrorResponse(
ErrorCode errorCode,
String traceId,
String path,
List<ErrorResponse.ValidationError> validationErrors
) {
return buildErrorResponse(errorCode, traceId, path, null, validationErrors, null);
}

private ErrorResponse buildErrorResponse(
ErrorCode errorCode,
String traceId,
String path,
String detailsOverride,
List<ErrorResponse.ValidationError> validationErrors,
Map<String, Object> additionalProperties
) {
String details = detailsOverride != null ? detailsOverride : errorCode.formatDetails();

return ErrorResponse.builder()
.traceId(traceId)
.status(errorCode.getHttpStatus().value())
.reason(errorCode.getHttpStatus().getReasonPhrase())
.code(errorCode.getCode())
.message(errorCode.getMessage())
.details(details)
.path(path)
.timestamp(Instant.now())
.validationErrors(validationErrors)
.additionalProperties(additionalProperties)
.build();
}

private ErrorResponse buildErrorResponseWithStatus(
ErrorCode errorCode,
String traceId,
String path,
String detailsOverride,
List<ErrorResponse.ValidationError> validationErrors,
Map<String, Object> additionalProperties,
int status,
String reason
) {
String details = detailsOverride != null ? detailsOverride : errorCode.formatDetails();

return ErrorResponse.builder()
.traceId(traceId)
.status(status)
.reason(reason)
.code(errorCode.getCode())
.message(errorCode.getMessage())
.details(details)
.path(path)
.timestamp(Instant.now())
.validationErrors(validationErrors)
.additionalProperties(additionalProperties)
.build();
}

private String resolveTraceId() {
return MDC.get(RequestIdFilter.MDC_REQUEST_ID_KEY);
}

private String resolvePath(WebRequest request) {
if (request instanceof ServletWebRequest servletWebRequest) {
return servletWebRequest.getRequest().getRequestURI();
}
return request.getDescription(false);
}

}



package com.azercell.akbintegrationservice.error.exceptions;

import com.azercell.akbintegrationservice.error.enums.ErrorCode;

public class AkbIntegrationException extends BaseException {

public AkbIntegrationException(String operation, Throwable cause) {
super(ErrorCode.AKB_INTEGRATION_ERROR, cause, operation);
withProperty("operation", operation);
}

public AkbIntegrationException(String operation, Long batchId, Throwable cause) {
super(ErrorCode.AKB_INTEGRATION_ERROR, cause, operation);
withProperty("operation", operation);
withProperty("batchId", batchId);
}

}


package com.azercell.akbintegrationservice.error.exceptions;

import com.azercell.akbintegrationservice.error.enums.ErrorCode;
import lombok.Getter;

import java.util.HashMap;
import java.util.Map;

@Getter
public class BaseException extends RuntimeException {

private final ErrorCode errorCode;
private final Map<String, Object> additionalProperties;
private final Object[] detailsParams;

public BaseException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.additionalProperties = new HashMap<>();
this.detailsParams = new Object[0];
}

public BaseException(ErrorCode errorCode, Object... detailsParams) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.additionalProperties = new HashMap<>();
this.detailsParams = detailsParams;
}

public BaseException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.additionalProperties = new HashMap<>();
this.detailsParams = new Object[0];
}

public BaseException(ErrorCode errorCode, Throwable cause, Object... detailsParams) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
this.additionalProperties = new HashMap<>();
this.detailsParams = detailsParams;
}

/**
* Add additional context
*/
public BaseException withProperty(String key, Object value) {
this.additionalProperties.put(key, value);
return this;
}

/**
* Get formatted details with params
*/
public String getFormattedDetails() {
return errorCode.formatDetails(detailsParams);
}

}


package com.azercell.akbintegrationservice.error.exceptions;

import com.azercell.akbintegrationservice.error.enums.ErrorCode;
import com.azercell.akbintegrationservice.error.model.ExternalServiceErrorResponse;
import lombok.Getter;

@Getter
public class ExternalServiceException extends BaseException {

private final ExternalServiceErrorResponse externalServiceError;

public ExternalServiceException(ExternalServiceErrorResponse externalServiceError) {
super(resolveErrorCode(externalServiceError));
this.externalServiceError = externalServiceError;
}

private static ErrorCode resolveErrorCode(ExternalServiceErrorResponse externalServiceError) {
if (externalServiceError != null && externalServiceError.getErrorCode() != null) {
return externalServiceError.getErrorCode();
}
return ErrorCode.AKB_INTEGRATION_ERROR;
}

}


package com.azercell.akbintegrationservice.error.exceptions;

import com.azercell.akbintegrationservice.error.enums.ErrorCode;

public class IdempotencyConflictException extends BaseException {

public IdempotencyConflictException(
String idempotencyKey,
String existingSubmissionId,
String existingStatus
) {
super(
ErrorCode.SUBMISSION_ALREADY_EXISTS,
idempotencyKey
);

withProperty("submissionId", existingSubmissionId);
withProperty("status", existingStatus);
withProperty("idempotencyKey", idempotencyKey);
}
}


package com.azercell.akbintegrationservice.error.exceptions;

import com.azercell.akbintegrationservice.error.enums.ErrorCode;

public class ScoringInquiryNotFoundException extends BaseException {

public ScoringInquiryNotFoundException(Long inquiryId) {
super(
ErrorCode.SCORING_INQUIRY_NOT_FOUND,
inquiryId
);
withProperty("inquiryId", inquiryId);
}
}


package com.azercell.akbintegrationservice.error.exceptions;

import com.azercell.akbintegrationservice.error.enums.ErrorCode;

public class SubmissionNotFoundException extends BaseException {

public SubmissionNotFoundException(String submissionId) {
super(
ErrorCode.SUBMISSION_NOT_FOUND,
submissionId
);
withProperty("submissionId", submissionId);
}
}





                                                                Localization and i18n

1. Ilk once Resource Bundle yaratmaliyiq

# en (default)

# Demo (optional test endpoint)
HELLO=Hello
GREETINGS=Hello, {0}!

# API errors
error.account.not_found=Account with ID ''{0}'' was not found.
error.validation.failed=Validation failed
error.internal_server_error=Internal server error
error.request.body.not_readable=Request body is missing or malformed.

# Bean validation (CreateAccountRequest)
validation.name.required=name must not be blank
validation.email.required=email must not be blank
validation.email.invalid=email must be valid
validation.balance.required=balance must not be null
validation.balance.positive=balance must be greater than zero


# az

# Demo
HELLO=Salam
GREETINGS=Salam, {0}!

# API errors
error.account.not_found=''{0}'' ID-li hesab tapılmadı.
error.validation.failed=Validasiya uğursuz oldu
error.internal_server_error=Daxili server xətası
error.request.body.not_readable=Sorğu gövdəsi yoxdur və ya səhv formatdadır.

# Bean validation (CreateAccountRequest)
validation.name.required=ad boş ola bilməz
validation.email.required=email boş ola bilməz
validation.email.invalid=email düzgün formatda deyil
validation.balance.required=balans boş ola bilməz
validation.balance.positive=balans sıfırdan böyük olmalıdır


# ru

# Demo
HELLO=Привет
GREETINGS=Привет, {0}!

# API errors
error.account.not_found=Счёт с ID ''{0}'' не найден.
error.validation.failed=Ошибка валидации
error.internal_server_error=Внутренняя ошибка сервера
error.request.body.not_readable=Тело запроса отсутствует или имеет неверный формат.

# Bean validation (CreateAccountRequest)
validation.name.required=имя не должно быть пустым
validation.email.required=email не должен быть пустым
validation.email.invalid=некорректный email
validation.balance.required=баланс не должен быть пустым
validation.balance.positive=баланс должен быть больше нуля


2. Novbeti addim yaml faylinda kofiqurasiyani elave etmek:

spring:
messages:
basename: message
encoding: UTF-8
web:
locale: en

bu addimda Spring-e deyirikki "tercumeler bu fayllardadir".


3. Novbeti addim bize LocaleResolver lazimdir, bu olmasa Spring bilmeyecek mesajlari hansi message faylindan oxusun. 

package guru.springframework.cruddemo.config;

import java.util.List;
import java.util.Locale;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;

@Configuration
public class LocaleConfig {

@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.ENGLISH);
resolver.setSupportedLocales(List.of(
Locale.forLanguageTag("az"),
Locale.forLanguageTag("ru"),
Locale.ENGLISH));
return resolver;
}

}


4. Novbeti addim ise bize MessageService lazimdir:

package guru.springframework.cruddemo.service.impl;

import java.util.Locale;

import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.WebRequest;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class TranslatorServiceImpl {

private final MessageSource messageSource;

public String translate(String key, Locale locale, Object... args) {
return messageSource.getMessage(key, args, key, locale);
}

public String translate(String key, WebRequest request, Object... args) {
return translate(key, request.getLocale(), args);
}

}


5. Novbeti addim ErroCode klasina messageKey field-ini elave etmek:

package guru.springframework.cruddemo.error;

import java.text.MessageFormat;
import java.util.Arrays;

import org.springframework.http.HttpStatus;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ErrorCode {

VALIDATION_FAILED(
"VALIDATION_FAILED",
"error.validation.failed",
"Validation failed",
"One or more fields failed validation.",
HttpStatus.BAD_REQUEST),
METHOD_VALIDATION_FAILED(
"METHOD_VALIDATION_FAILED",
"error.method.validation.failed",
"Method validation failed",
"One or more method parameters failed validation.",
HttpStatus.BAD_REQUEST),
CONSTRAINT_VIOLATION(
"CONSTRAINT_VIOLATION",
"error.constraint.violation",
"Constraint violation",
"One or more constraints were violated.",
HttpStatus.BAD_REQUEST),
REQUEST_BODY_NOT_READABLE(
"REQUEST_BODY_NOT_READABLE",
"error.request.body.not_readable",
"Malformed request body",
"Request body is missing or malformed.",
HttpStatus.BAD_REQUEST),
MISSING_REQUEST_PARAMETER(
"MISSING_REQUEST_PARAMETER",
"error.missing.request.parameter",
"Missing request parameter",
"A required request parameter is missing.",
HttpStatus.BAD_REQUEST),
TYPE_MISMATCH(
"TYPE_MISMATCH",
"error.type.mismatch",
"Type mismatch",
"Request parameter type is invalid.",
HttpStatus.BAD_REQUEST),
BAD_REQUEST(
"BAD_REQUEST",
"error.bad.request",
"Bad request",
"Request is malformed or contains invalid data.",
HttpStatus.BAD_REQUEST),
AKB_INTEGRATION_ERROR(
"AKB_INTEGRATION_ERROR",
"error.akb.integration.error",
"AKB integration error",
"AKB integration failed during {0}.",
HttpStatus.SERVICE_UNAVAILABLE),
INTERNAL_SERVER_ERROR(
"INTERNAL_SERVER_ERROR",
"error.internal_server_error",
"Internal server error",
"An unexpected error occurred.",
HttpStatus.INTERNAL_SERVER_ERROR),
SUBMISSION_ALREADY_EXISTS(
"IDEMPOTENCY_KEY_EXISTS",
"error.submission.already.exists",
"Submission with this idempotency key already exists",
"A credit submission with idempotency key ''{0}'' has already been processed. This is a duplicate request. If you need to submit a new credit, please generate a new idempotency key.",
HttpStatus.CONFLICT),
SUBMISSION_NOT_FOUND(
"SUBMISSION_NOT_FOUND",
"error.submission.not_found",
"Submission not found",
"Submission with ID ''{0}'' was not found.",
HttpStatus.NOT_FOUND),
SCORING_INQUIRY_NOT_FOUND(
"SCORING_INQUIRY_NOT_FOUND",
"error.scoring.inquiry.not_found",
"Scoring inquiry not found",
"Scoring inquiry with ID ''{0}'' was not found.",
HttpStatus.NOT_FOUND),
ACCOUNT_NOT_FOUND(
"ACCOUNT_NOT_FOUND",
"error.account.not_found",
"Account not found",
"Account with ID ''{0}'' was not found.",
HttpStatus.NOT_FOUND),

UNKNOWN(
"UNKNOWN",
"error.unknown",
"Unknown error",
"An unrecognized error code was received.",
HttpStatus.INTERNAL_SERVER_ERROR);

@Getter(onMethod_ = @JsonValue)
private final String code;
private final String messageKey;
private final String message;
private final String detailsTemplate;
private final HttpStatus httpStatus;

public String formatDetails(Object... params) {
if (params == null || params.length == 0) {
return this.detailsTemplate;
}
return MessageFormat.format(this.detailsTemplate, params);
}

@JsonCreator
public static ErrorCode fromCode(String value) {
if (value == null || value.isBlank()) {
return null;
}
return Arrays.stream(values())
.filter(c -> c.code.equals(value))
.findFirst()
.orElse(UNKNOWN);
}
}



6. Validasiya (field error) mesajlarini i18n ile birleshdirmek. 

package guru.springframework.cruddemo.dto;

import java.math.BigDecimal;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;

@Data
public class CreateAccountRequest {

@NotBlank(message = "{validation.name.required}")
private String name;

@NotBlank(message = "{validation.email.required}")
@Email(message = "{validation.email.invalid}")
private String email;

@NotNull(message = "{validation.balance.required}")
@Positive(message = "{validation.balance.positive}")
private BigDecimal balance;

}


ve ValidationConfig quraq:

package guru.springframework.cruddemo.config;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class ValidationConfig {

@Bean
public LocalValidatorFactoryBean defaultValidator(MessageSource messageSource) {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setValidationMessageSource(messageSource);
return factoryBean;
}

}


sonra ise GlobalExceptionHandler-de tetbiq etmek:


package guru.springframework.cruddemo.error;

import java.time.Instant;
import java.util.List;
import java.util.Locale;

import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;

import guru.springframework.cruddemo.service.impl.TranslatorServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {

private final TranslatorServiceImpl translator;

@ExceptionHandler(BaseException.class)
public ResponseEntity<ErrorResponse> handleBaseException(BaseException ex, WebRequest request) {
Locale locale = request.getLocale();
ErrorResponse body = ErrorResponse.builder()
.status(ex.getHttpStatus().value())
.code(ex.getErrorCode().getCode())
.message(translator.translate(
ex.getErrorCode().getMessageKey(), locale, ex.getDetailsParams()))
.timestamp(Instant.now())
.path(requestPath(request))
.build();
return ResponseEntity.status(ex.getHttpStatus()).body(body);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
WebRequest request) {
Locale locale = request.getLocale();
ErrorCode code = ErrorCode.VALIDATION_FAILED;
List<FieldErrorDto> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
.map(fieldError -> FieldErrorDto.builder()
.field(fieldError.getField())
.message(resolveFieldMessage(fieldError.getDefaultMessage(), locale))
.build())
.toList();
ErrorResponse body = ErrorResponse.builder()
.status(code.getHttpStatus().value())
.code(code.getCode())
.message(translator.translate(code.getMessageKey(), locale))
.timestamp(Instant.now())
.path(requestPath(request))
.fieldErrors(fieldErrors)
.build();
return ResponseEntity.status(code.getHttpStatus()).body(body);
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ErrorResponse> handleHttpMessageNotReadable(
HttpMessageNotReadableException ex,
WebRequest request) {
Locale locale = request.getLocale();
ErrorCode code = ErrorCode.REQUEST_BODY_NOT_READABLE;
ErrorResponse body = ErrorResponse.builder()
.status(code.getHttpStatus().value())
.code(code.getCode())
.message(translator.translate(code.getMessageKey(), locale))
.timestamp(Instant.now())
.path(requestPath(request))
.build();
return ResponseEntity.status(code.getHttpStatus()).body(body);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex, WebRequest request) {
log.error("Unhandled exception", ex);
Locale locale = request.getLocale();
ErrorCode code = ErrorCode.INTERNAL_SERVER_ERROR;
ErrorResponse body = ErrorResponse.builder()
.status(code.getHttpStatus().value())
.code(code.getCode())
.message(translator.translate(code.getMessageKey(), locale))
.timestamp(Instant.now())
.path(requestPath(request))
.build();
return ResponseEntity.status(code.getHttpStatus()).body(body);
}

private String resolveFieldMessage(String defaultMessage, Locale locale) {
if (defaultMessage == null || defaultMessage.isBlank()) {
return defaultMessage;
}
if (defaultMessage.startsWith("{") && defaultMessage.endsWith("}")) {
String key = defaultMessage.substring(1, defaultMessage.length() - 1);
return translator.translate(key, locale);
}
return defaultMessage;
}

private static String requestPath(WebRequest request) {
if (request instanceof ServletWebRequest servletWebRequest) {
return servletWebRequest.getRequest().getRequestURI();
}
return request.getDescription(false);
}

}










==========================================================================

package com.azercell.akbintegrationservice.client.akb.decoder;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AkbErrorResponse {

private String timestamp;
private Integer status;
private String error;
private String message;
private String path;

}


package com.azercell.akbintegrationservice.client.akb.decoder;

import com.azercell.akbintegrationservice.error.enums.ErrorCode;
import com.azercell.akbintegrationservice.error.exceptions.ExternalServiceException;
import com.azercell.akbintegrationservice.error.model.ExternalServiceErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.URI;
import java.time.Instant;

@Slf4j
public class AkbErrorDecoder implements ErrorDecoder {

private final ObjectMapper objectMapper;

public AkbErrorDecoder(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Override
public Exception decode(String methodKey, Response response) {
String body = readBody(response);
ExternalServiceErrorResponse externalError = mapExternalError(response, body);
return new ExternalServiceException(externalError);
}

private ExternalServiceErrorResponse mapExternalError(Response response, String body) {
String path = resolvePath(response);
String timestamp = Instant.now().toString();

if (body != null && !body.isBlank()) {
try {
AkbErrorResponse akbError = objectMapper.readValue(body, AkbErrorResponse.class);
return ExternalServiceErrorResponse.builder()
.timestamp(akbError.getTimestamp() != null ? akbError.getTimestamp() : timestamp)
.status(akbError.getStatus() != null ? akbError.getStatus() : response.status())
.error(akbError.getError())
.message(akbError.getMessage())
.path(akbError.getPath() != null ? akbError.getPath() : path)
.errorCode(ErrorCode.AKB_INTEGRATION_ERROR)
.build();
} catch (Exception ex) {
log.error("Failed to parse AKB error response body, falling back", ex);
// Fall through to fallback mapping
}
}

return ExternalServiceErrorResponse.builder()
.timestamp(timestamp)
.status(response.status())
.error(response.reason())
.message(body)
.path(path)
.errorCode(ErrorCode.AKB_INTEGRATION_ERROR)
.build();
}

private String resolvePath(Response response) {
if (response.request() == null || response.request().url() == null) {
return null;
}
try {
return URI.create(response.request().url()).getPath();
} catch (Exception ignored) {
return response.request().url();
}
}

private String readBody(Response response) {
if (response.body() == null) {
return null;
}
try {
return Util.toString(response.body().asReader(Util.UTF_8));
} catch (IOException ignored) {
return null;
}
}

}






























Комментарии

Популярные сообщения из этого блога

Interview questions

Lesson1: JDK, JVM, JRE

Lesson_2: Operations in Java