Skip to content

Centralize Exception Handling & Standardize API Error Responses#132

Open
diyajn wants to merge 1 commit into
rdodiya:gssoc_developfrom
diyajn:fix/centralize-exception-handling
Open

Centralize Exception Handling & Standardize API Error Responses#132
diyajn wants to merge 1 commit into
rdodiya:gssoc_developfrom
diyajn:fix/centralize-exception-handling

Conversation

@diyajn
Copy link
Copy Markdown

@diyajn diyajn commented May 17, 2026

Closes #83


📌 What Was Done

Point 1 — Centralized exception handling in one place

Created a dedicated com.restroly.qrmenu.exception package as the single
source of truth for all exceptions. Removed scattered exception classes from
common/exception/ and user/exception/. All exceptions now flow through
one GlobalExceptionHandler with @RestControllerAdvice — zero
@ExceptionHandler annotations exist anywhere else in the codebase.

Point 2 — Created a reusable ApiErrorResponse DTO

Introduced ApiErrorResponse.java with standardized fields:
timestamp, status, error, message, path, traceId,
and validationErrors (for field-level errors). Used consistently
as the return type across all 16 exception handlers.

Point 3 — Standardized validation, auth, and generic error responses

Every category of error now returns a consistent ApiErrorResponse:

  • @Valid / @Validated failures → 400 with validationErrors[] list
  • BadCredentialsException (wrong login) → 401 Unauthorized
  • AuthenticationException (no token) → 401 Unauthorized
  • AccessDeniedException (wrong role) → 403 Forbidden
  • Unhandled exceptions → 500 with unique traceId for log correlation

Point 4 — Consistent across all APIs

Replaced all raw RuntimeException, IllegalStateException throws with
proper typed exceptions:

  • RoleServiceImpl: IllegalStateExceptionBusinessException (409)
  • OrderItemBuilder: IllegalStateExceptionValidationException (400)
  • SectionServiceImpl: RuntimeExceptionResourceNotFoundException (404)
  • CloudinaryService: RuntimeExceptionBusinessException (503)

All 13 services and builders now throw only from
com.restroly.qrmenu.exception.* — guaranteed to be handled.

Point 5 — Exception responses tested across major endpoints

Verified consistent ApiErrorResponse JSON returned for:

Scenario Endpoint HTTP
Validation failure POST /users with empty body 400 + validationErrors[]
Resource not found GET /users/99999 404
Duplicate resource POST /users with existing email 409
Bad login POST /auth/login wrong password 401
No token Any secured endpoint 401
Wrong role Admin endpoint as USER 403
Malformed JSON Any POST with broken JSON 400

Files Changed

  • exception/ — new centralized package (11 files)
  • user/service/RoleServiceImpl.javaIllegalStateExceptionBusinessException
  • order/builder/OrderItemBuilder.javaIllegalStateExceptionValidationException
  • template/service/SectionServiceImpl.javaRuntimeExceptionResourceNotFoundException
  • config/CloudinaryService.javaRuntimeExceptionBusinessException

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR centralizes exception types under com.restroly.qrmenu.exception and standardizes HTTP error payloads using a single ApiErrorResponse DTO, with errors emitted consistently via a @RestControllerAdvice GlobalExceptionHandler.

Changes:

  • Introduces ApiErrorResponse and updates GlobalExceptionHandler to return it for validation, security, business, and generic errors.
  • Moves/renames exception classes into a single com.restroly.qrmenu.exception package and updates service/controller imports accordingly.
  • Replaces several generic RuntimeException/IllegalStateException throws with typed business/validation/not-found exceptions.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
RestroHub/src/main/java/com/restroly/qrmenu/user/service/UserServiceImpl.java Updates imports to centralized exception package.
RestroHub/src/main/java/com/restroly/qrmenu/user/service/RoleServiceImpl.java Uses centralized exceptions; replaces IllegalStateException with BusinessException.
RestroHub/src/main/java/com/restroly/qrmenu/template/service/TemplateServiceImpl.java Switches to centralized Resource* exceptions.
RestroHub/src/main/java/com/restroly/qrmenu/template/service/SiteConfigServiceImpl.java Switches to centralized Resource* exceptions.
RestroHub/src/main/java/com/restroly/qrmenu/template/service/SectionServiceImpl.java Uses centralized exceptions; replaces RuntimeException with ResourceNotFoundException.
RestroHub/src/main/java/com/restroly/qrmenu/restaurant/service/RestaurantServiceImpl.java Switches to centralized Resource* exceptions.
RestroHub/src/main/java/com/restroly/qrmenu/restaurant/controller/RestaurantController.java Updates OpenAPI error schema references to ApiErrorResponse.
RestroHub/src/main/java/com/restroly/qrmenu/order/service/impl/OrderServiceImpl.java Switches to centralized ResourceNotFoundException import; removes unused imports.
RestroHub/src/main/java/com/restroly/qrmenu/order/builder/OrderItemBuilder.java Replaces IllegalStateException with ValidationException.
RestroHub/src/main/java/com/restroly/qrmenu/menu/service/MenuServiceImpl.java Switches to centralized ResourceNotFoundException / DuplicateResourceException.
RestroHub/src/main/java/com/restroly/qrmenu/menu/controller/MenuController.java Updates OpenAPI error schema references to ApiErrorResponse.
RestroHub/src/main/java/com/restroly/qrmenu/food/service/FoodServiceImpl.java Switches to centralized Resource* exceptions; removes unused imports.
RestroHub/src/main/java/com/restroly/qrmenu/food/controller/FoodController.java Updates OpenAPI error schema references to ApiErrorResponse; cleans imports.
RestroHub/src/main/java/com/restroly/qrmenu/exception/ValidationException.java Moves exception and ties validation error list type to ApiErrorResponse.ValidationError.
RestroHub/src/main/java/com/restroly/qrmenu/exception/UserNotFoundException.java Moves exception; changes to extend ResourceNotFoundException.
RestroHub/src/main/java/com/restroly/qrmenu/exception/RoleNotFoundException.java Moves exception; changes to extend ResourceNotFoundException.
RestroHub/src/main/java/com/restroly/qrmenu/exception/ResourceNotFoundException.java Moves exception into centralized package.
RestroHub/src/main/java/com/restroly/qrmenu/exception/ResourceAlreadyExistsException.java Moves exception into centralized package.
RestroHub/src/main/java/com/restroly/qrmenu/exception/GlobalExceptionHandler.java Centralized @ExceptionHandler methods returning ApiErrorResponse.
RestroHub/src/main/java/com/restroly/qrmenu/exception/DuplicateResourceException.java Moves exception; changes to extend ResourceAlreadyExistsException.
RestroHub/src/main/java/com/restroly/qrmenu/exception/BusinessException.java Moves exception into centralized package.
RestroHub/src/main/java/com/restroly/qrmenu/exception/ApiErrorResponse.java Introduces standardized error response DTO.
RestroHub/src/main/java/com/restroly/qrmenu/config/CloudinaryService.java Replaces RuntimeException with BusinessException for upload failures.
RestroHub/src/main/java/com/restroly/qrmenu/category/service/CategoryServiceImpl.java Switches to centralized ResourceNotFoundException.
RestroHub/src/main/java/com/restroly/qrmenu/branch/service/BranchServiceImpl.java Switches to centralized ResourceNotFoundException / DuplicateResourceException.
RestroHub/src/main/java/com/restroly/qrmenu/branch/controller/BranchController.java Updates OpenAPI error schema references to ApiErrorResponse.
RestroHub/src/main/java/com/restroly/qrmenu/auth/service/AuthServiceImpl.java Switches to centralized BusinessException.
RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java Updates OpenAPI error schema references to ApiErrorResponse.
Comments suppressed due to low confidence (5)

RestroHub/src/main/java/com/restroly/qrmenu/restaurant/controller/RestaurantController.java:81

  • getRestaurantById is missing a Spring mapping annotation (e.g., @GetMapping("/{restaurantId}")). As written, this method won’t be exposed as an endpoint, despite having OpenAPI annotations and a @PathVariable parameter.
                    content = @Content(schema = @Schema(implementation = ApiErrorResponse.class)))
    })
    public ResponseEntity<RestaurantResponseDTO> getRestaurantById(
            @Parameter(description = "Long Id of the restaurant", required = true)
            @PathVariable Long restaurantId) {

RestroHub/src/main/java/com/restroly/qrmenu/exception/GlobalExceptionHandler.java:310

  • A new traceId is generated for the response here, but that traceId is not logged for most handled exceptions (only the generic 500 handler logs it). This makes client-provided traceId values hard to correlate with server logs; consider generating the traceId once per handler and including it in the corresponding log line (or returning it from buildErrorResponse and logging it).
    RestroHub/src/main/java/com/restroly/qrmenu/exception/GlobalExceptionHandler.java:229
  • NoHandlerFoundException is only thrown if Spring MVC is configured to do so (e.g., spring.mvc.throw-exception-if-no-handler-found=true and disabling default resource mappings). Without that configuration, 404s for unknown routes won’t pass through this handler and will not return ApiErrorResponse.
    RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java:86
  • The documented 401 example doesn’t match the actual ApiErrorResponse produced by GlobalExceptionHandler: error is set to HttpStatus.UNAUTHORIZED.getReasonPhrase() ("Unauthorized"), not "UNAUTHORIZED", and the hard-coded path should match the controller’s @RequestMapping (based on PUBLIC_API_VERSION). Please update the example to reflect the real response shape/values.
                                        "status": 401,
                                        "error": "UNAUTHORIZED",
                                        "message": "Invalid username or password",
                                        "path": "/api/v1/auth/login",
                                        "timestamp": "2024-01-15T10:30:00",

RestroHub/src/main/java/com/restroly/qrmenu/exception/GlobalExceptionHandler.java:80

  • BusinessException carries an errorCode, but the handler currently discards it when building ApiErrorResponse. If clients are expected to rely on codes like "ROLE_IN_USE" / "IMAGE_UPLOAD_FAILED", add an errorCode field to ApiErrorResponse and populate it here; otherwise consider removing errorCode from the exception to avoid unused signaling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 69 to +73
SiteConfig siteConfig = siteConfigRepository.findBySiteId(siteId)
.orElseThrow(() -> new ResourceNotFoundException("Site config not found with siteId : "+ siteId));

SectionTemplate template = sectionTemplateRepository.findById(sectionTemplateId)
.orElseThrow(() -> new RuntimeException("Section template not found: " + sectionTemplateId));

.orElseThrow(() -> new ResourceNotFoundException("SectionTemplate", "id", sectionTemplateId));
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add Global Exception Handler for consistent API error responses

2 participants