Centralize Exception Handling & Standardize API Error Responses#132
Open
diyajn wants to merge 1 commit into
Open
Centralize Exception Handling & Standardize API Error Responses#132diyajn wants to merge 1 commit into
diyajn wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
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
ApiErrorResponseand updatesGlobalExceptionHandlerto return it for validation, security, business, and generic errors. - Moves/renames exception classes into a single
com.restroly.qrmenu.exceptionpackage and updates service/controller imports accordingly. - Replaces several generic
RuntimeException/IllegalStateExceptionthrows 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
getRestaurantByIdis 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@PathVariableparameter.
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
traceIdis generated for the response here, but thattraceIdis not logged for most handled exceptions (only the generic 500 handler logs it). This makes client-providedtraceIdvalues hard to correlate with server logs; consider generating thetraceIdonce per handler and including it in the corresponding log line (or returning it frombuildErrorResponseand logging it).
RestroHub/src/main/java/com/restroly/qrmenu/exception/GlobalExceptionHandler.java:229 NoHandlerFoundExceptionis only thrown if Spring MVC is configured to do so (e.g.,spring.mvc.throw-exception-if-no-handler-found=trueand disabling default resource mappings). Without that configuration, 404s for unknown routes won’t pass through this handler and will not returnApiErrorResponse.
RestroHub/src/main/java/com/restroly/qrmenu/auth/controller/AuthController.java:86- The documented 401 example doesn’t match the actual
ApiErrorResponseproduced byGlobalExceptionHandler:erroris set toHttpStatus.UNAUTHORIZED.getReasonPhrase()("Unauthorized"), not "UNAUTHORIZED", and the hard-codedpathshould match the controller’s@RequestMapping(based onPUBLIC_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
BusinessExceptioncarries anerrorCode, but the handler currently discards it when buildingApiErrorResponse. If clients are expected to rely on codes like "ROLE_IN_USE" / "IMAGE_UPLOAD_FAILED", add anerrorCodefield toApiErrorResponseand populate it here; otherwise consider removingerrorCodefrom 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)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #83
📌 What Was Done
Point 1 — Centralized exception handling in one place
Created a dedicated
com.restroly.qrmenu.exceptionpackage as the singlesource of truth for all exceptions. Removed scattered exception classes from
common/exception/anduser/exception/. All exceptions now flow throughone
GlobalExceptionHandlerwith@RestControllerAdvice— zero@ExceptionHandlerannotations exist anywhere else in the codebase.Point 2 — Created a reusable
ApiErrorResponseDTOIntroduced
ApiErrorResponse.javawith standardized fields:timestamp,status,error,message,path,traceId,and
validationErrors(for field-level errors). Used consistentlyas 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/@Validatedfailures → 400 withvalidationErrors[]listBadCredentialsException(wrong login) → 401 UnauthorizedAuthenticationException(no token) → 401 UnauthorizedAccessDeniedException(wrong role) → 403 ForbiddentraceIdfor log correlationPoint 4 — Consistent across all APIs
Replaced all raw
RuntimeException,IllegalStateExceptionthrows withproper typed exceptions:
RoleServiceImpl:IllegalStateException→BusinessException(409)OrderItemBuilder:IllegalStateException→ValidationException(400)SectionServiceImpl:RuntimeException→ResourceNotFoundException(404)CloudinaryService:RuntimeException→BusinessException(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
ApiErrorResponseJSON returned for:POST /userswith empty bodyvalidationErrors[]GET /users/99999POST /userswith existing emailPOST /auth/loginwrong passwordFiles Changed
exception/— new centralized package (11 files)user/service/RoleServiceImpl.java—IllegalStateException→BusinessExceptionorder/builder/OrderItemBuilder.java—IllegalStateException→ValidationExceptiontemplate/service/SectionServiceImpl.java—RuntimeException→ResourceNotFoundExceptionconfig/CloudinaryService.java—RuntimeException→BusinessException