← Back to Documentation Portal|📊 Coverage Quality Gates|Coverage Dashboard
Payflow API — Automated JaCoCo Code Coverage (90% Line / 73% Branch)

GlobalExceptionHandler.java

package com.payflow.exception;

import java.net.URI;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;

import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import jakarta.validation.ConstraintViolationException;

@RestControllerAdvice
public class GlobalExceptionHandler {

	private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);

	@ExceptionHandler(UserNotFoundException.class)
	public ProblemDetail handleUserNotFound(UserNotFoundException ex) {
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
		problem.setType(URI.create("https://api.payflow.com/errors/user-not-found"));
		problem.setTitle("User Not Found");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(TransactionNotFoundException.class)
	public ProblemDetail handleTransactionNotFound(TransactionNotFoundException ex) {
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
		problem.setType(URI.create("https://api.payflow.com/errors/transaction-not-found"));
		problem.setTitle("Transaction Not Found");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(DuplicateUpiIdException.class)
	public ProblemDetail handleDuplicateUpiId(DuplicateUpiIdException ex) {
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
		problem.setType(URI.create("https://api.payflow.com/errors/duplicate-upi-id"));
		problem.setTitle("Duplicate UPI ID");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(InsufficientBalanceException.class)
	public ProblemDetail handleInsufficientBalance(InsufficientBalanceException ex) {
		HttpStatus status = HttpStatus.UNPROCESSABLE_ENTITY;
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, ex.getMessage());
		problem.setType(URI.create("https://api.payflow.com/errors/insufficient-balance"));
		problem.setTitle("Insufficient Balance");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(SelfTransferException.class)
	public ProblemDetail handleSelfTransfer(SelfTransferException ex) {
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
		problem.setType(URI.create("https://api.payflow.com/errors/self-transfer-prohibited"));
		problem.setTitle("Self Transfer Prohibited");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(ForbiddenOperationException.class)
	public ProblemDetail handleForbiddenOperation(ForbiddenOperationException ex) {
		LOG.warn("Forbidden operation: {}", ex.getMessage());
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, ex.getMessage());
		problem.setType(URI.create("https://api.payflow.com/errors/forbidden-operation"));
		problem.setTitle("Forbidden Operation");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(AccessDeniedException.class)
	public ProblemDetail handleAccessDenied(AccessDeniedException ex) {
		LOG.warn("Access denied: {}", ex.getMessage());
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.FORBIDDEN, ex.getMessage());
		problem.setType(URI.create("https://api.payflow.com/errors/access-denied"));
		problem.setTitle("Access Denied");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(FeatureDisabledException.class)
	public ProblemDetail handleFeatureDisabled(FeatureDisabledException ex) {
		LOG.warn("Feature disabled access attempt: {}", ex.getMessage());
		HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE;
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(status, ex.getMessage());
		problem.setType(URI.create("https://api.payflow.com/errors/feature-disabled"));
		problem.setTitle("Feature Disabled");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(InvalidUpiException.class)
	public ProblemDetail handleInvalidUpi(InvalidUpiException ex) {
		LOG.warn("Invalid UPI: {}", ex.getMessage());
		String detail = ex.getMessage();
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, detail);
		problem.setType(URI.create("https://api.payflow.com/errors/invalid-upi-id"));
		problem.setTitle("Invalid UPI ID");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(MethodArgumentNotValidException.class)
	public ProblemDetail handleValidationException(MethodArgumentNotValidException ex) {
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY,
				"Validation failed for request parameters");
		problem.setType(URI.create("https://api.payflow.com/errors/validation-error"));
		problem.setTitle("Validation Failure");

		Map<String, String> fieldErrors = new HashMap<>();
		for (FieldError error : ex.getBindingResult().getFieldErrors()) {
			fieldErrors.put(error.getField(), error.getDefaultMessage());
		}
		problem.setProperty("errors", fieldErrors);
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(HandlerMethodValidationException.class)
	public ProblemDetail handleHandlerMethodValidationException(HandlerMethodValidationException ex) {
		LOG.warn("Handler method validation failed: {}", ex.getMessage());
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY,
				"Validation failed for request parameters");
		problem.setType(URI.create("https://api.payflow.com/errors/validation-error"));
		problem.setTitle("Validation Failure");

		Map<String, String> paramErrors = new HashMap<>();
		for (var result : ex.getParameterValidationResults()) {
			String paramName = result.getMethodParameter().getParameterName();
			String fieldName = paramName != null ? paramName : "parameter";
			for (var resolvableError : result.getResolvableErrors()) {
				paramErrors.put(fieldName, resolvableError.getDefaultMessage());
			}
		}
		if (!paramErrors.isEmpty()) {
			problem.setProperty("errors", paramErrors);
		}
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(ConstraintViolationException.class)
	public ProblemDetail handleConstraintViolationException(ConstraintViolationException ex) {
		LOG.warn("Constraint violation: {}", ex.getMessage());
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY,
				"Validation constraint violated");
		problem.setType(URI.create("https://api.payflow.com/errors/validation-error"));
		problem.setTitle("Validation Failure");

		Map<String, String> violations = new HashMap<>();
		for (var violation : ex.getConstraintViolations()) {
			var propPath = violation.getPropertyPath();
			String path = propPath != null ? propPath.toString() : "parameter";
			violations.put(path, violation.getMessage());
		}
		problem.setProperty("errors", violations);
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(MethodArgumentTypeMismatchException.class)
	public ProblemDetail handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex) {
		LOG.warn("Parameter type mismatch for {}: {}", ex.getName(), ex.getMessage());
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST,
				String.format("Invalid value '%s' for parameter '%s'", ex.getValue(), ex.getName()));
		problem.setType(URI.create("https://api.payflow.com/errors/type-mismatch"));
		problem.setTitle("Parameter Type Mismatch");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(DataIntegrityViolationException.class)
	public ProblemDetail handleDataIntegrityViolation(DataIntegrityViolationException ex) {
		LOG.warn("Data integrity violation: {}", ex.getMessage());
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT,
				"Database constraint violation occurred");
		problem.setType(URI.create("https://api.payflow.com/errors/data-integrity-violation"));
		problem.setTitle("Data Integrity Violation");
		enrichProblemDetail(problem);
		return problem;
	}

	@ExceptionHandler(RequestNotPermitted.class)
	public ResponseEntity<ProblemDetail> handleRequestNotPermitted(RequestNotPermitted ex) {
		LOG.warn("Rate limit exceeded: {}", ex.getMessage());
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.TOO_MANY_REQUESTS,
				"Rate limit exceeded. Maximum 10 requests per second allowed per user.");
		problem.setType(URI.create("https://api.payflow.com/errors/rate-limit-exceeded"));
		problem.setTitle("Rate Limit Exceeded");
		enrichProblemDetail(problem);
		var response = ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).header(HttpHeaders.RETRY_AFTER, "1");
		return response.body(problem);
	}

	@ExceptionHandler(CallNotPermittedException.class)
	public ResponseEntity<ProblemDetail> handleCallNotPermitted(CallNotPermittedException ex) {
		LOG.error("Circuit breaker is open: {}", ex.getMessage());
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.SERVICE_UNAVAILABLE,
				"External service circuit breaker is open. Service temporarily unavailable.");
		problem.setType(URI.create("https://api.payflow.com/errors/service-unavailable"));
		problem.setTitle("Service Unavailable");
		enrichProblemDetail(problem);
		return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(problem);
	}

	@ExceptionHandler(Exception.class)
	public ProblemDetail handleUncaughtException(Exception ex) {
		LOG.error("Unhandled server exception: ", ex);
		ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR,
				"An unexpected internal error occurred");
		problem.setType(URI.create("https://api.payflow.com/errors/internal-server-error"));
		problem.setTitle("Internal Server Error");
		enrichProblemDetail(problem);
		return problem;
	}

	private void enrichProblemDetail(ProblemDetail problem) {
		problem.setProperty("timestamp", Instant.now().toString());
		String requestId = MDC.get("requestId");
		if (requestId != null) {
			problem.setProperty("requestId", requestId);
		}
	}
}