Skip to content

Add LlmClientException for handling client error responses and upda…#6

Merged
mattknatt merged 2 commits into
mainfrom
fix/improve-exception-handling
May 5, 2026
Merged

Add LlmClientException for handling client error responses and upda…#6
mattknatt merged 2 commits into
mainfrom
fix/improve-exception-handling

Conversation

@mattknatt

@mattknatt mattknatt commented May 5, 2026

Copy link
Copy Markdown
Owner

…te exception handling across components and tests.

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced HTTP error handling to properly distinguish between client errors (4xx responses) and server unavailability (5xx/429 responses).
    • Client-side errors now provide detailed status code information in error messages.
    • Server unavailability scenarios maintain automatic retry behavior.
  • Tests

    • Added comprehensive test coverage for retry mechanisms, error handling scenarios, and recovery paths.

…te exception handling across components and tests.
@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

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

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ce442df1-9d8b-4f82-b48e-860259d68a4b

📥 Commits

Reviewing files that changed from the base of the PR and between 46a1f71 and 9ac0aa1.

📒 Files selected for processing (4)
  • src/main/java/org/example/springllmgateway/client/LlmClient.java
  • src/main/java/org/example/springllmgateway/exception/GlobalExceptionHandler.java
  • src/main/java/org/example/springllmgateway/exception/LlmClientException.java
  • src/test/java/org/example/springllmgateway/controller/ChatControllerTest.java
📝 Walkthrough

Walkthrough

A new exception type LlmClientException is introduced to handle 4xx HTTP responses. Error mapping in LlmClient.sendMessages() is expanded to distinguish 4xx client errors from 5xx/429 availability errors. A new global exception handler maps LlmClientException to HTTP 502. Tests validate the new exception flow and retry behavior.

Changes

HTTP Error Handling Enhancement

Layer / File(s) Summary
Exception Type Definition
src/main/java/org/example/springllmgateway/exception/LlmClientException.java
New LlmClientException class extending RuntimeException with a String message constructor.
Error Mapping Logic
src/main/java/org/example/springllmgateway/client/LlmClient.java
sendMessages() error handling expands from only 429/503 to 429 or any 5xx as LlmUnavailableException. Non-429 4xx responses now map to LlmClientException with status code in the message.
Exception Handler Wiring
src/main/java/org/example/springllmgateway/exception/GlobalExceptionHandler.java
New @ExceptionHandler for LlmClientException returns HTTP 502 BAD_GATEWAY with timestamp, status, and message fields.
Test Coverage
src/test/java/org/example/springllmgateway/client/LlmClientTest.java, src/test/java/org/example/springllmgateway/controller/ChatControllerTest.java
Tests added for 400/401 throwing LlmClientException, retry scenarios for 429/500, retry exhaustion, and controller-level 502 response mapping.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Controller
    participant Service
    participant LlmClient
    participant RemoteAPI as Remote LLM API
    participant GlobalHandler as GlobalExceptionHandler

    Note over Client,GlobalHandler: Old Flow (429 only)
    Client->>Controller: POST /api/v1/chat
    Controller->>Service: chat(request)
    Service->>LlmClient: sendMessages()
    LlmClient->>RemoteAPI: HTTP POST
    RemoteAPI-->>LlmClient: 429 or 503
    LlmClient-->>Service: throws LlmUnavailableException
    Service-->>Controller: propagate exception
    Controller->>GlobalHandler: handleLlmUnavailable()
    GlobalHandler-->>Client: 503 Service Unavailable

    rect rgba(100, 150, 200, 0.5)
    Note over Client,GlobalHandler: New Flow (4xx vs 5xx)
    Client->>Controller: POST /api/v1/chat
    Controller->>Service: chat(request)
    Service->>LlmClient: sendMessages()
    LlmClient->>RemoteAPI: HTTP POST
    alt 4xx (400, 401, etc.)
        RemoteAPI-->>LlmClient: 4xx Client Error
        LlmClient-->>Service: throws LlmClientException
        Service-->>Controller: propagate exception
        Controller->>GlobalHandler: handleLlmClientError()
        GlobalHandler-->>Client: 502 Bad Gateway
    else 429 or 5xx
        RemoteAPI-->>LlmClient: 429 or 5xx Server Error
        LlmClient-->>Service: throws LlmUnavailableException
        Service-->>Controller: propagate exception
        Controller->>GlobalHandler: handleLlmUnavailable()
        GlobalHandler-->>Client: 503 Service Unavailable
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • mattknatt/spring-llm-gateway#1: Both PRs modify LlmClient.sendMessages() error-handling logic and GlobalExceptionHandler, extending HTTP error mapping and exception handling in parallel.

Poem

A client error springs forth with grace,
Four-oh-four finds its rightful place,
While five-oh-threes still fade away—
The gateway sorts them, come what may. 🐰

🚥 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 clearly summarizes the main change: introducing LlmClientException for handling client error responses and updating exception handling across components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 fix/improve-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.

🧹 Nitpick comments (2)
src/main/java/org/example/springllmgateway/exception/GlobalExceptionHandler.java (1)

17-24: 💤 Low value

Consider revisiting the HTTP 502 status for LlmClientException.

HTTP 502 semantically means "received an invalid response from an upstream server." Here the upstream LLM returned a perfectly valid 4xx response — it rejected a request. Returning 502 makes LlmClientException indistinguishable from a network-level bad-gateway scenario from a monitoring/alerting perspective, and it conflates two different failure modes with LlmUnavailableException → 503.

Alternatives that are arguably more accurate:

  • 400 if a malformed payload from the original client reached the LLM.
  • 422 for semantically invalid requests the LLM rejected.
  • 401 / 403 pass-through if the root cause is a credentials/auth misconfiguration.

If the deliberate choice is to hide upstream status codes from callers (a valid gateway pattern), at minimum document why 502 was chosen over 503 or 400 for discoverability.

src/test/java/org/example/springllmgateway/client/LlmClientTest.java (1)

142-151: ⚡ Quick win

hasSize(4) assertion is correct; heads-up on real-delay cost.

Spring Framework's @Retryable counts maxRetries as retries in addition to the initial invocation — total attempts = 1 + maxRetries. With maxRetries = 3, the exhaustion test correctly expects 4 total WireMock calls.

However, with delay = 1000, multiplier = 2, and three retries, this test incurs real wall-clock delays of approximately 1 000 ms + 2 000 ms + 4 000 ms ≈ 7+ seconds per run. Combined with the two new single-retry tests (~1 s each), the new tests add ~9 s to the @SpringBootTest suite. If CI run-time becomes a concern, consider externalizing the retry delay to a property (e.g., llm.retry.delay) so it can be overridden to 0 or 1 in application-test.properties.

💡 Example: externalize delay for test override

In LlmClient.java:

 `@Retryable`(
         includes = LlmUnavailableException.class,
         maxRetries = 3,
-        delay = 1000,
+        delayString = "${llm.retry.delay:1000}",
         multiplier = 2,
         jitter = 200,
         maxDelay = 5000
 )

Then in src/test/resources/application-test.properties (activated via @SpringBootTest):

llm.retry.delay=1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/org/example/springllmgateway/client/LlmClientTest.java` around
lines 142 - 151, The test causes real retry delays because the `@Retryable`
settings in LlmClient use fixed delay/multiplier; externalize the retry timing
into a configurable property (e.g., llm.retry.delay and llm.retry.multiplier)
used by the `@Retryable` on the method in LlmClient so tests can override it, then
add application-test.properties (activated by the test profile) setting
llm.retry.delay=1 (or 0) and llm.retry.multiplier=1 to eliminate long waits
during CI; update any references to the retry attributes in LlmClient to read
from the new properties (or from a `@ConfigurationProperties/Environment`) so
runtime behavior is unchanged but tests can shorten delays.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/test/java/org/example/springllmgateway/client/LlmClientTest.java`:
- Around line 142-151: The test causes real retry delays because the `@Retryable`
settings in LlmClient use fixed delay/multiplier; externalize the retry timing
into a configurable property (e.g., llm.retry.delay and llm.retry.multiplier)
used by the `@Retryable` on the method in LlmClient so tests can override it, then
add application-test.properties (activated by the test profile) setting
llm.retry.delay=1 (or 0) and llm.retry.multiplier=1 to eliminate long waits
during CI; update any references to the retry attributes in LlmClient to read
from the new properties (or from a `@ConfigurationProperties/Environment`) so
runtime behavior is unchanged but tests can shorten delays.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f851cc4-5322-4178-b538-880df6ab342a

📥 Commits

Reviewing files that changed from the base of the PR and between e491c45 and 46a1f71.

📒 Files selected for processing (5)
  • src/main/java/org/example/springllmgateway/client/LlmClient.java
  • src/main/java/org/example/springllmgateway/exception/GlobalExceptionHandler.java
  • src/main/java/org/example/springllmgateway/exception/LlmClientException.java
  • src/test/java/org/example/springllmgateway/client/LlmClientTest.java
  • src/test/java/org/example/springllmgateway/controller/ChatControllerTest.java

… handling in `GlobalExceptionHandler`, and add unit tests for 4xx status mappings
@mattknatt
mattknatt merged commit 50b49d7 into main May 5, 2026
2 checks passed
@mattknatt
mattknatt deleted the fix/improve-exception-handling branch May 5, 2026 13:37
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.

1 participant