Skip to content

#48 exception handling#57

Merged
mattknatt merged 5 commits into
mainfrom
#48-exception-handling
Apr 16, 2026
Merged

#48 exception handling#57
mattknatt merged 5 commits into
mainfrom
#48-exception-handling

Conversation

@Tyreviel

@Tyreviel Tyreviel commented Apr 15, 2026

Copy link
Copy Markdown
Collaborator

closes #48

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced error response handling with appropriate HTTP status codes for validation failures and authorization issues.
  • New Features

    • Added dedicated error page for application errors with clear status and message display.
  • Tests

    • Added test coverage for error scenarios in REST and web controllers.

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Tyreviel has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 43 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 8 minutes and 43 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 443242a0-d8d6-4eb4-b947-4f998f9b4249

📥 Commits

Reviewing files that changed from the base of the PR and between f376c69 and f207ab8.

📒 Files selected for processing (4)
  • src/main/java/org/example/projektarendehantering/application/service/CaseService.java
  • src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java
  • src/test/java/org/example/projektarendehantering/DocumentIntegrationTest.java
  • src/test/java/org/example/projektarendehantering/presentation/rest/CaseControllerTest.java
📝 Walkthrough

Walkthrough

The PR refactors exception handling by replacing Spring's generic ResponseStatusException with domain-specific custom exceptions (BadRequestException, ConflictException, NotAuthorizedException), annotating them with @ResponseStatus for HTTP status mapping. A centralized GlobalControllerAdvice controller with multiple @ExceptionHandler methods now differentiates between REST and non-REST requests, returning JSON for API calls and rendering an error.html template for web page requests.

Changes

Cohort / File(s) Summary
Service Exception Handling
src/main/java/org/example/projektarendehantering/application/service/CaseService.java, src/main/java/org/example/projektarendehantering/application/service/PatientService.java
Replaced Spring's ResponseStatusException with domain-specific exceptions: BadRequestException in CaseService.createCase for invalid patientId, and ConflictException in PatientService.createPatient for duplicate personalIdentityNumber. Removed unused Spring HTTP imports.
Custom Exception Classes
src/main/java/org/example/projektarendehantering/common/BadRequestException.java, src/main/java/org/example/projektarendehantering/common/ConflictException.java, src/main/java/org/example/projektarendehantering/common/NotAuthorizedException.java
Added @ResponseStatus annotations to exception classes for automatic HTTP status mapping (400 for BadRequestException, 409 for ConflictException, 403 for NotAuthorizedException). New ConflictException class with two constructors for flexible error code handling.
Centralized Exception Handling
src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java
Introduced controller advice with four @ExceptionHandler methods detecting REST vs. non-REST requests via URI pattern matching (/api/ prefix). Returns JSON ResponseEntity for REST calls and HTTP status + model attributes + error view for non-REST calls. Extracts HTTP status from @ResponseStatus annotation or defaults to 500.
Error Template & Tests
src/main/resources/templates/error.html, src/test/java/org/example/projektarendehantering/presentation/rest/CaseControllerTest.java, src/test/java/org/example/projektarendehantering/presentation/web/UiControllerTest.java
Added Thymeleaf error template with status, message, and conditional error code display. New REST API test validates NotAuthorizedException returns 403 JSON response. New MVC web test validates non-REST error handling returns 403 with error view and model attributes.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant Service as CaseService
    participant ExceptionHandler as GlobalControllerAdvice
    participant Response as HTTP Response

    Note over Client,Response: REST Request Flow (GET /api/cases/{id})
    Client->>Service: getCase(doctorActor, caseId)
    alt Unauthorized Access
        Service-->>ExceptionHandler: throws NotAuthorizedException
        ExceptionHandler->>ExceptionHandler: isRestRequest() = true
        ExceptionHandler->>Response: ResponseEntity.status(403).body({errorCode, message})
        Response-->>Client: HTTP 403 + JSON
    end

    Note over Client,Response: Non-REST Request Flow (GET /ui/cases/{id})
    Client->>Service: getCase(doctorActor, caseId)
    alt Unauthorized Access
        Service-->>ExceptionHandler: throws NotAuthorizedException
        ExceptionHandler->>ExceptionHandler: isRestRequest() = false
        ExceptionHandler->>ExceptionHandler: Extract `@ResponseStatus`(403)
        ExceptionHandler->>Response: response.setStatus(403)<br/>model.addAttribute(status, message, errorCode)<br/>return "error" view
        Response-->>Client: HTTP 403 + error.html page
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 Exceptions once scattered, now gathered with care,
Domain-specific throwers dance through the air!
REST gets JSON, web gets a page,
GlobalControllerAdvice tames the error stage! 🎭

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '#48 exception handling' is directly related to the main changes in this PR, which centralize exception handling by adding domain-specific exceptions and a global controller advice.
Linked Issues check ✅ Passed All coding requirements from issue #48 are met: domain-specific exceptions (BadRequestException, ConflictException, NotAuthorizedException) with proper HTTP mappings, centralized exception handling via GlobalControllerAdvice, and error page UI for unauthorized access.
Out of Scope Changes check ✅ Passed All changes are in-scope for improving exception handling; modifications to CaseService and PatientService use the new exceptions, tests verify error behavior, and the error.html template displays proper error messages.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch #48-exception-handling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/org/example/projektarendehantering/common/NotAuthorizedException.java (1)

12-13: ⚠️ Potential issue | 🟠 Major

Standardize unauthorized errorCode to one canonical value.

Line 13 defaults to "NOT_AUTHORIZED", while new tests and mocked throws in this PR use "NOT_ALLOWED". This creates inconsistent API error codes for the same class of failure.

💡 Suggested fix
 public class NotAuthorizedException extends AppException {
+    public static final String ERROR_CODE = "NOT_ALLOWED";
+
     public NotAuthorizedException(String errorCode, String message) {
         super(errorCode, message);
     }

     public NotAuthorizedException(String message) {
-        super("NOT_AUTHORIZED", message);
+        super(ERROR_CODE, message);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/common/NotAuthorizedException.java`
around lines 12 - 13, Constructor in NotAuthorizedException currently passes
"NOT_AUTHORIZED" as the errorCode which conflicts with the canonical code used
elsewhere ("NOT_ALLOWED"); update the NotAuthorizedException(String message)
constructor to call super with the canonical error code "NOT_ALLOWED" instead of
"NOT_AUTHORIZED" (i.e., change the super("NOT_AUTHORIZED", message) call in
class NotAuthorizedException to use "NOT_ALLOWED") so the API errorCode matches
tests and mocked throws.
🧹 Nitpick comments (2)
src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java (2)

82-88: REST response schema inconsistent with other handlers.

This handler omits the errorCode field from the JSON response, while handleAppException, handleAccessDeniedException, and handleGeneralException all include it. Clients expecting a uniform error schema may encounter issues.

Consider adding a generic error code for consistency:

♻️ Proposed fix
         if (isRestRequest(request)) {
             return ResponseEntity.status(code)
                     .body(Map.of(
+                            "errorCode", "RESPONSE_STATUS_ERROR",
                             "message", reason,
                             "status", code.value()
                     ));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java`
around lines 82 - 88, The REST branch in GlobalControllerAdvice currently builds
a Map with "message" and "status" but omits "errorCode", causing an inconsistent
schema; update the isRestRequest(request) response construction (inside
GlobalControllerAdvice) to include an "errorCode" entry (e.g. a generic value
like "UNKNOWN_ERROR" or a constant used by other handlers) so it matches
handleAppException/handleAccessDeniedException/handleGeneralException’s response
shape.

119-121: Consider context path in REST request detection.

If the application is deployed with a context path (e.g., /myapp), the URI would be /myapp/api/... and this check would fail. Using request.getServletPath() or checking against the configured API base path would be more robust.

♻️ Alternative using servlet path
 private boolean isRestRequest(HttpServletRequest request) {
-    return request.getRequestURI().startsWith("/api/");
+    return request.getServletPath().startsWith("/api/");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java`
around lines 119 - 121, The isRestRequest(HttpServletRequest request) method
incorrectly inspects request.getRequestURI() which breaks when the app has a
context path; change the detection to use request.getServletPath() (e.g., check
request.getServletPath().startsWith("/api/")) or construct the match using
request.getContextPath() + "/api/" so the method correctly detects REST calls
regardless of deployment context; update the isRestRequest implementation and
any callers accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java`:
- Around line 101-117: The global exception handler handleGeneralException in
GlobalControllerAdvice swallows exceptions without logging; add a private static
final Logger (LoggerFactory.getLogger(GlobalControllerAdvice.class)) to the
class and log the caught Exception e (including stack trace) at an appropriate
level (e.g., log.error) at the start of handleGeneralException before building
the response or model so production errors are recorded for debugging.

In `@src/main/resources/templates/error.html`:
- Line 11: Replace the current Thymeleaf attribute on the <p class="muted" ...>
element so it uses an explicit fallback expression (ternary or Elvis) instead of
plain th:text="${message}"; for example set th:text to "${message} ?: 'An
unexpected error occurred.'", ensuring the literal fallback text is shown when
message is null or empty.

---

Outside diff comments:
In
`@src/main/java/org/example/projektarendehantering/common/NotAuthorizedException.java`:
- Around line 12-13: Constructor in NotAuthorizedException currently passes
"NOT_AUTHORIZED" as the errorCode which conflicts with the canonical code used
elsewhere ("NOT_ALLOWED"); update the NotAuthorizedException(String message)
constructor to call super with the canonical error code "NOT_ALLOWED" instead of
"NOT_AUTHORIZED" (i.e., change the super("NOT_AUTHORIZED", message) call in
class NotAuthorizedException to use "NOT_ALLOWED") so the API errorCode matches
tests and mocked throws.

---

Nitpick comments:
In
`@src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java`:
- Around line 82-88: The REST branch in GlobalControllerAdvice currently builds
a Map with "message" and "status" but omits "errorCode", causing an inconsistent
schema; update the isRestRequest(request) response construction (inside
GlobalControllerAdvice) to include an "errorCode" entry (e.g. a generic value
like "UNKNOWN_ERROR" or a constant used by other handlers) so it matches
handleAppException/handleAccessDeniedException/handleGeneralException’s response
shape.
- Around line 119-121: The isRestRequest(HttpServletRequest request) method
incorrectly inspects request.getRequestURI() which breaks when the app has a
context path; change the detection to use request.getServletPath() (e.g., check
request.getServletPath().startsWith("/api/")) or construct the match using
request.getContextPath() + "/api/" so the method correctly detects REST calls
regardless of deployment context; update the isRestRequest implementation and
any callers accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7f1cb877-04c4-4ca2-97a5-aedeeae466da

📥 Commits

Reviewing files that changed from the base of the PR and between 20877c4 and f376c69.

📒 Files selected for processing (9)
  • src/main/java/org/example/projektarendehantering/application/service/CaseService.java
  • src/main/java/org/example/projektarendehantering/application/service/PatientService.java
  • src/main/java/org/example/projektarendehantering/common/BadRequestException.java
  • src/main/java/org/example/projektarendehantering/common/ConflictException.java
  • src/main/java/org/example/projektarendehantering/common/NotAuthorizedException.java
  • src/main/java/org/example/projektarendehantering/presentation/web/GlobalControllerAdvice.java
  • src/main/resources/templates/error.html
  • src/test/java/org/example/projektarendehantering/presentation/rest/CaseControllerTest.java
  • src/test/java/org/example/projektarendehantering/presentation/web/UiControllerTest.java

<main class="container page">
<div class="panel panel-error" style="text-align: center; padding: 40px;">
<h1 th:text="${status != null ? status : 'Error'}">Error</h1>
<p class="muted" style="font-size: 1.2rem; margin-top: 16px;" th:text="${message}">An unexpected error occurred.</p>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Thymeleaf, when th:text points to a null model attribute, does the element body fallback text render, or is it replaced by an empty string?

💡 Result:

In Thymeleaf, when th:text points to a null model attribute, the element body fallback text does NOT render. It is replaced by an empty string.

Citations:


Use an explicit fallback expression to ensure message displays reliably.

Line 11: th:text="${message}" renders an empty string when message is null, so the literal fallback text inside the <p> tag is ignored. Use a ternary operator to provide an explicit fallback.

Suggested fix
-        <p class="muted" style="font-size: 1.2rem; margin-top: 16px;" th:text="${message}">An unexpected error occurred.</p>
+        <p class="muted" style="font-size: 1.2rem; margin-top: 16px;"
+           th:text="${message != null and !#strings.isEmpty(message) ? message : 'An unexpected error occurred.'}">
+            An unexpected error occurred.
+        </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/error.html` at line 11, Replace the current
Thymeleaf attribute on the <p class="muted" ...> element so it uses an explicit
fallback expression (ternary or Elvis) instead of plain th:text="${message}";
for example set th:text to "${message} ?: 'An unexpected error occurred.'",
ensuring the literal fallback text is shown when message is null or empty.

@mattknatt mattknatt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Very nice! LGTM

@mattknatt
mattknatt merged commit 278b0a0 into main Apr 16, 2026
2 checks passed
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.

Improve exception handling, display proper messages when unauthorized etc

2 participants