Skip to content

Implement retry, backoff#5

Merged
codebyNorthsteep merged 9 commits into
mainfrom
feature/implement-retry-backoff
May 8, 2026
Merged

Implement retry, backoff#5
codebyNorthsteep merged 9 commits into
mainfrom
feature/implement-retry-backoff

Conversation

@codebyNorthsteep

@codebyNorthsteep codebyNorthsteep commented May 7, 2026

Copy link
Copy Markdown
Owner

This pull request introduces circuit breaker and retry mechanisms for improved resilience when communicating with the LLM service, updates dependencies to support these features, and adds integration tests to verify the retry logic. The changes also include some refactoring and enhancements for clarity and maintainability.

Resilience and Reliability Enhancements:

  • Added Resilience4j circuit breaker and retry annotations to ChatService, specifically to the fetchResponseFromLLM method, to handle failures and retries when communicating with the LLM service (src/main/java/org/example/projectbifrost/service/ChatService.java). [1] [2]
  • Configured circuit breaker and retry settings in application.properties for fine-tuned failure handling (src/main/resources/application.properties).

Dependency and Build Updates:

  • Added spring-cloud-starter-circuitbreaker-reactor-resilience4j and wiremock-spring-boot dependencies, and imported spring-cloud-dependencies BOM for dependency management in pom.xml. [1] [2] [3]
  • Removed unnecessary spring-restdocs-asciidoctor dependency from the build plugins section in pom.xml.

Testing Improvements:

  • Introduced an integration test using WireMock to verify the retry mechanism in ChatServiceTest, ensuring that the service retries failed requests and succeeds when the LLM becomes available (src/test/java/org/example/projectbifrost/service/ChatServiceTest.java).

Code Refactoring and Cleanup:

  • Renamed sendRequestToLLM to chatWithLLM in ChatService and updated references in BifrostController for clarity and consistency (src/main/java/org/example/projectbifrost/service/ChatService.java, src/main/java/org/example/projectbifrost/BifrostController.java). [1] [2]
  • Renamed openAIWebClient bean to openRouterRestClient for better alignment with its purpose (src/main/java/org/example/projectbifrost/configuration/RestClientConfiguration.java).

These updates collectively improve the robustness of the chat service, ensure graceful handling of LLM outages, and provide automated tests to validate the new behavior.

Summary by CodeRabbit

  • New Features

    • Added circuit-breaker and automatic retry for chat calls with a graceful fallback response when the LLM is unavailable; improved LLM response validation and clearer 503 service-unavailable handling.
    • Switched to an OpenRouter-based LLM client and adjusted HTTP client behavior.
  • Chores

    • Updated build configuration to include resilience, testing, and related runtime libraries.
  • Tests

    • Added integration tests validating retry and circuit-breaker behavior against a mock LLM endpoint.

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

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

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ 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: afc9987a-1550-41f0-9505-d5529dfac667

📥 Commits

Reviewing files that changed from the base of the PR and between 96925bc and d930cfa.

📒 Files selected for processing (2)
  • src/main/java/org/example/projectbifrost/service/ChatService.java
  • src/test/java/org/example/projectbifrost/service/ChatServiceTest.java
📝 Walkthrough

Walkthrough

The PR integrates Resilience4j circuit breaker and retry into the ChatService LLM integration, renames and reconfigures the RestClient bean, adds new exception types and handlers, updates POM dependencies (Spring Cloud BOM, AspectJ, WireMock, circuit-breaker starter), adds Resilience4j properties and OpenRouter base URL, updates controller wiring, and adds WireMock-based integration tests.

Changes

Resilience4j Circuit Breaker & Retry Integration

Layer / File(s) Summary
Build & Dependency Configuration
pom.xml
Spring Cloud BOM version 2025.1.1 is added; spring-boot-starter-aspectj, wiremock-spring-boot:4.0.9, and spring-cloud-starter-circuitbreaker-reactor-resilience4j dependencies are introduced.
RestClient Bean Construction
src/main/java/.../configuration/RestClientConfiguration.java
Bean renamed to openRouterRestClient(RestClient.Builder) and wired with an Apache HttpClient (automatic retries disabled), baseUrl, and default headers.
New Exception Types & Handlers
src/main/java/.../exception/*
Adds InvalidLLMResponseException (holds model/statusCode) and RetryableHttpException; GlobalExceptionHandler updated to handle them (map invalid responses to status-based ProblemDetail; retryable failures to 503).
ChatService Refactoring with Resilience Patterns
src/main/java/.../service/ChatService.java
chatWithLLM(ChatRequestDTO) replaces sendRequestToLLM(); fetchResponseFromLLM(List<Message>) is annotated with @CircuitBreaker and @Retry; retryable HTTP errors limited to 429/500 via RetryableHttpException; response validation throws InvalidLLMResponseException; fallback returns "Fallback!".
Controller Integration
src/main/java/.../BifrostController.java
/api/v1/chat endpoint now invokes chatService.chatWithLLM(dto) instead of sendRequestToLLM(dto).
Resilience4j & OpenRouter Configuration
src/main/resources/application.properties
Adds openrouter.base-url, Resilience4j circuit breaker and retry settings for chatService, and sets io.github.resilience4j logging to DEBUG.
Integration Testing with WireMock
src/test/java/.../service/ChatServiceTest.java
Adds tests that stub OpenRouter /chat/completions to validate retry (500,500,200) and circuit-breaker fallback/open-state behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • codebyNorthsteep/ProjectBifrost#1: Directly related; refactors the same ChatService, RestClientConfiguration, and BifrostController with overlapping changes to method names and LLM communication patterns.
  • codebyNorthsteep/ProjectBifrost#3: Related to ChatService error handling and LLM request/response logic; that PR adds LLMException-based HTTP error handling while this PR replaces that with InvalidLLMResponseException and Resilience4j handling.

Poem

🐰 I hopped through retries, three by three,

Circuit-breaker watched and kept me free,
WireMock sang its scripted tune,
chatWithLLM returned at noon,
A little rabbit logs — "Fallback!" — then hops on.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Implement retry, backoff' is vague and generic, using non-descriptive terms that don't clearly convey what the changeset accomplishes. Consider a more specific title like 'Add Resilience4j circuit breaker and retry patterns to ChatService' to better describe the main change and its scope.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 feature/implement-retry-backoff

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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: 4

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/projectbifrost/service/ChatService.java (1)

47-65: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

@CircuitBreaker and @Retry are completely bypassed in the production path — self-invocation skips the Spring AOP proxy.

chatWithLLM calls fetchResponseFromLLM(apiMessages) on this, the raw bean instance. Spring's proxy-based AOP only intercepts calls made through the proxy object, not through direct this references. The result is that every call entering via the controller goes through chatWithLLMthis.fetchResponseFromLLM(...), and no retry or circuit breaker logic ever fires.

The test passes because ChatServiceTest autowires the proxy and calls fetchResponseFromLLM directly on it, which does go through the AOP machinery. But that test path is not the production path.

The cleanest fix is to extract fetchResponseFromLLM into a separate @Service bean and inject it into ChatService:

🐛 Proposed fix – extract to a dedicated bean

New bean (e.g. LLMClient.java):

`@Service`
public class LLMClient {

    private final RestClient restClient;

    `@Value`("${openrouter.model}")
    private String model;

    public LLMClient(RestClient restClient) {
        this.restClient = restClient;
    }

    `@CircuitBreaker`(name = "chatService", fallbackMethod = "fallback")
    `@Retry`(name = "chatService")
    public String fetchResponseFromLLM(List<OpenRouterRequestDTO.Message> apiMessages) {
        // ... same body ...
    }

    public String fallback(List<OpenRouterRequestDTO.Message> apiMessages, Exception e) {
        log.warn("LLM unavailable, returning fallback", e);
        return "Fallback!";
    }
}

ChatService – inject LLMClient and delegate:

-    String content = fetchResponseFromLLM(apiMessages);
+    String content = llmClient.fetchResponseFromLLM(apiMessages);

Alternatively, if a separate class is undesirable, inject a self reference via @Lazy @Autowired private ChatService self and call self.fetchResponseFromLLM(...).

🤖 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/main/java/org/example/projectbifrost/service/ChatService.java` around
lines 47 - 65, chatWithLLM is calling fetchResponseFromLLM on the same
ChatService instance, which bypasses Spring AOP proxies so `@CircuitBreaker` and
`@Retry` never run; extract fetchResponseFromLLM into a separate Spring bean
(e.g., LLMClient with a public
fetchResponseFromLLM(List<OpenRouterRequestDTO.Message>) method annotated with
`@CircuitBreaker` and `@Retry` and a fallback method) and inject that bean into
ChatService, then have chatWithLLM delegate to the injected
LLMClient.fetchResponseFromLLM(...) (alternatively inject a proxied self
reference with `@Lazy` to call self.fetchResponseFromLLM(...)).
🧹 Nitpick comments (2)
pom.xml (1)

115-118: ⚡ Quick win

Consider using the non-reactive circuit breaker starter for this synchronous service.

ChatService.fetchResponseFromLLM returns a plain String via a synchronous RestClient. The Spring Cloud docs recommend spring-cloud-starter-circuitbreaker-resilience4j for non-reactive applications, and spring-cloud-starter-circuitbreaker-reactor-resilience4j for reactive applications. The Reactor variant adds a ReactiveResilience4JCircuitBreakerFactory bean and pulls in WebFlux-related circuit breaker infrastructure that is unused here.

♻️ Proposed change
         <dependency>
             <groupId>org.springframework.cloud</groupId>
-            <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
+            <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
         </dependency>
🤖 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 `@pom.xml` around lines 115 - 118, The POM currently pulls the reactive
circuit-breaker starter (artifactId
spring-cloud-starter-circuitbreaker-reactor-resilience4j) while this service is
synchronous; change the dependency to the non-reactive starter (artifactId
spring-cloud-starter-circuitbreaker-resilience4j) so the app doesn't register
Reactor/WebFlux circuit-breaker infrastructure that
ChatService.fetchResponseFromLLM (synchronous RestClient) doesn't use; update
the dependency entry in pom.xml accordingly and run a build to ensure no unused
reactive classes remain referenced.
src/main/java/org/example/projectbifrost/service/ChatService.java (1)

92-94: ⚡ Quick win

Fallback silently swallows failures — no logging makes production incidents invisible.

When the circuit is open or all retries are exhausted, the caller receives the generic string "Fallback!" with no trace in the logs. Add at minimum a log.warn or log.error with the exception so that repeated fallback activations are observable.

⚙️ Proposed fix
+    private static final Logger log = LoggerFactory.getLogger(ChatService.class);
+    // (or use `@Slf4j` from Lombok)
+
     public String fallback(List<OpenRouterRequestDTO.Message> apiMessages, Exception e) {
+        log.error("LLM call failed after retries/circuit open, returning fallback. Cause: {}", e.getMessage(), e);
         return "Fallback!";
     }
🤖 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/main/java/org/example/projectbifrost/service/ChatService.java` around
lines 92 - 94, The fallback method currently returns a generic string and
swallows the exception; update the
ChatService.fallback(List<OpenRouterRequestDTO.Message> apiMessages, Exception
e) to log the failure (use log.warn or log.error) and include the exception as
the throwable parameter plus brief context (e.g., apiMessages size or a short
summary) so repeated fallbacks are observable in logs; keep the existing return
value or make it slightly more descriptive if desired, but do not remove the
exception logging.
🤖 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.

Inline comments:
In `@pom.xml`:
- Around line 101-105: The WireMock dependency (groupId
org.wiremock.integrations, artifactId wiremock-spring-boot, version 4.0.9) is
currently declared without a test scope and will be packaged into production
artifacts; update the dependency declaration to add a <scope>test</scope> so
wiremock-spring-boot is only available at test time and not on the
compile/runtime classpath or in the produced JAR/WAR.

In `@src/main/java/org/example/projectbifrost/service/ChatService.java`:
- Line 15: Remove the unused import
org.springframework.resilience.annotation.Retryable from the ChatService class:
locate the import statement at the top of ChatService.java and delete it
(keeping the io.github.resilience4j.retry.annotation.Retry usage intact); then
run an import-organize or build to ensure no other unused imports remain and
compilation passes.

In `@src/main/resources/application.properties`:
- Line 11: The retry config for the "chatService" instance is too broad and
immediate: update the resilience4j properties for
resilience4j.retry.instances.chatService to (1) add retry-exceptions or
ignore-exceptions so only transient failures are retried (e.g.,
retryExceptions=LLMException or use ignoreExceptions=SomeClientErrorException)
or configure a predicate that filters by HTTP status instead of retrying all
exceptions thrown by fetchResponseFromLLM, and (2) add a wait-duration (e.g.,
exponential/backoff-friendly value) so attempts are spaced out; reference the
chatService instance and the fetchResponseFromLLM / LLMException symbols when
making these property changes.

In `@src/test/java/org/example/projectbifrost/service/ChatServiceTest.java`:
- Around line 54-56: The assertion in ChatServiceTest uses
findAll(getRequestedFor("/chat/completions")) which matches GETs while the
service sends POSTs (so it always returns empty) and also duplicates the
existing verify(3, postRequestedFor(...)) check; fix by either replacing
getRequestedFor(...) with postRequestedFor(...) inside findAll to correctly
match POST requests for the endpoint, or simply remove the findAll(...)
assertion (lines calling findAll/getRequestedFor) since verify(3,
postRequestedFor(...)) already asserts the same behavior.

---

Outside diff comments:
In `@src/main/java/org/example/projectbifrost/service/ChatService.java`:
- Around line 47-65: chatWithLLM is calling fetchResponseFromLLM on the same
ChatService instance, which bypasses Spring AOP proxies so `@CircuitBreaker` and
`@Retry` never run; extract fetchResponseFromLLM into a separate Spring bean
(e.g., LLMClient with a public
fetchResponseFromLLM(List<OpenRouterRequestDTO.Message>) method annotated with
`@CircuitBreaker` and `@Retry` and a fallback method) and inject that bean into
ChatService, then have chatWithLLM delegate to the injected
LLMClient.fetchResponseFromLLM(...) (alternatively inject a proxied self
reference with `@Lazy` to call self.fetchResponseFromLLM(...)).

---

Nitpick comments:
In `@pom.xml`:
- Around line 115-118: The POM currently pulls the reactive circuit-breaker
starter (artifactId spring-cloud-starter-circuitbreaker-reactor-resilience4j)
while this service is synchronous; change the dependency to the non-reactive
starter (artifactId spring-cloud-starter-circuitbreaker-resilience4j) so the app
doesn't register Reactor/WebFlux circuit-breaker infrastructure that
ChatService.fetchResponseFromLLM (synchronous RestClient) doesn't use; update
the dependency entry in pom.xml accordingly and run a build to ensure no unused
reactive classes remain referenced.

In `@src/main/java/org/example/projectbifrost/service/ChatService.java`:
- Around line 92-94: The fallback method currently returns a generic string and
swallows the exception; update the
ChatService.fallback(List<OpenRouterRequestDTO.Message> apiMessages, Exception
e) to log the failure (use log.warn or log.error) and include the exception as
the throwable parameter plus brief context (e.g., apiMessages size or a short
summary) so repeated fallbacks are observable in logs; keep the existing return
value or make it slightly more descriptive if desired, but do not remove the
exception logging.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0c641f49-873b-4dfe-b8ba-a16fea73d2aa

📥 Commits

Reviewing files that changed from the base of the PR and between 1a56b11 and f6d860e.

📒 Files selected for processing (6)
  • pom.xml
  • src/main/java/org/example/projectbifrost/BifrostController.java
  • src/main/java/org/example/projectbifrost/configuration/RestClientConfiguration.java
  • src/main/java/org/example/projectbifrost/service/ChatService.java
  • src/main/resources/application.properties
  • src/test/java/org/example/projectbifrost/service/ChatServiceTest.java

Comment thread pom.xml
Comment thread src/main/java/org/example/projectbifrost/service/ChatService.java Outdated
Comment thread src/main/resources/application.properties
Comment thread src/test/java/org/example/projectbifrost/service/ChatServiceTest.java Outdated
…r LLM errors with retry support. Enhance `RestClientConfiguration` with resilient methods and `HttpComponentsClientHttpRequestFactory`. Update global exception handler and tests.

@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: 3

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/projectbifrost/service/ChatService.java (1)

46-64: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical: self-invocation bypasses Resilience4j AOP — retry/circuit breaker won't fire in production.

chatWithLLM calls fetchResponseFromLLM(apiMessages) directly via implicit this, which bypasses the Spring AOP proxy. Resilience4j's @CircuitBreaker and @Retry are proxy-based, so they are skipped on internal calls — meaning the controller flow gets neither retries nor circuit breaking, and fallback(...) will never be invoked along this path. Your ChatServiceTest likely passes because it invokes fetchResponseFromLLM externally on the autowired bean, masking this bug.

Major: fallback method silently swallows exceptions with no logging. The fallback accepts an Exception e parameter but discards it entirely, returning "Fallback!" with zero observability. This hides the root cause of failures and makes debugging production issues extremely difficult.

Minor: RetryableHttpException loses HTTP status code and response details. The onStatus lambda throws the exception with a hardcoded message but doesn't preserve the original status code (429 vs 500) or response body, losing debugging context.

Remediate by moving fetchResponseFromLLM to a separate Spring bean (preferred — clean separation), or use self-injection / AopContext.currentProxy(). Add logging to the fallback method to record the exception cause.

♻️ Proposed fix (self-injection example)
 `@Service`
 public class ChatService {

     private final ChatSessionStorage chatSessionStorage;
     private final RestClient restClient;
+    private final ApplicationContext applicationContext;
+    private ChatService self;

     `@Value`("${openrouter.model}")
     private String model;

-    public ChatService(ChatSessionStorage chatSessionStorage, RestClient restClient) {
+    public ChatService(ChatSessionStorage chatSessionStorage,
+                       RestClient restClient,
+                       ApplicationContext applicationContext) {
         this.chatSessionStorage = chatSessionStorage;
         this.restClient = restClient;
+        this.applicationContext = applicationContext;
+    }
+
+    `@PostConstruct`
+    void initSelf() {
+        this.self = applicationContext.getBean(ChatService.class);
     }
@@
-        String content = fetchResponseFromLLM(apiMessages);
+        String content = self.fetchResponseFromLLM(apiMessages);
+
+    public String fallback(List<OpenRouterRequestDTO.Message> apiMessages, Exception e) {
+        log.error("LLM circuit breaker opened, fallback invoked", e);
+        return "Fallback!";
     }

The cleaner long-term fix is to extract fetchResponseFromLLM (and fallback) into a dedicated LLMClient bean and inject it.

🧹 Nitpick comments (2)
src/main/java/org/example/projectbifrost/service/ChatService.java (1)

36-45: 💤 Low value

Javadoc no longer reflects the exception contract.

The javadoc claims only InvalidLLMResponseException is thrown, but fetchResponseFromLLM can also raise RetryableHttpException (and any unexpected RestClient errors), and — depending on how the self-invocation issue is resolved — may instead silently return "Fallback!". Tighten the @throws and behavioral notes once the proxy/fallback design is finalized.

🤖 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/main/java/org/example/projectbifrost/service/ChatService.java` around
lines 36 - 45, Update the Javadoc for ChatService.sendChatRequest to accurately
reflect all exceptions and behaviors: mention that fetchResponseFromLLM may
throw RetryableHttpException and other RestClientException types in addition to
InvalidLLMResponseException, and note any fallback string behavior (e.g.,
returning "Fallback!" if applicable) until the proxy/fallback design is
finalized; reference the sendChatRequest method, fetchResponseFromLLM,
RetryableHttpException, RestClientException, and InvalidLLMResponseException so
callers know what to expect and when to handle retries or fallbacks.
src/main/java/org/example/projectbifrost/configuration/RestClientConfiguration.java (1)

8-14: ⚡ Quick win

Remove unused @EnableResilientMethods annotation.

@EnableResilientMethods only activates Spring's native @Retryable and @ConcurrencyLimit, neither of which are used in this codebase. Your ChatService uses Resilience4j's @CircuitBreaker and @Retry annotations, which are wired automatically by the resilience4j-spring-boot3 starter. Remove the unused annotation and import.

Proposed fix
-import org.springframework.resilience.annotation.EnableResilientMethods;
 import org.springframework.web.client.RestClient;

 import java.net.URI;

 `@Configuration`
-@EnableResilientMethods
 public class RestClientConfiguration {
🤖 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/main/java/org/example/projectbifrost/configuration/RestClientConfiguration.java`
around lines 8 - 14, Remove the unused Spring annotation and import: the
`@EnableResilientMethods` import and annotation on RestClientConfiguration are not
needed because the project uses Resilience4j annotations (`@CircuitBreaker`,
`@Retry`) wired by resilience4j-spring-boot3; open the RestClientConfiguration
class, delete the import for
org.springframework.resilience.annotation.EnableResilientMethods and remove the
`@EnableResilientMethods` annotation declaration so only needed imports and the
`@Configuration` remain.
🤖 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.

Inline comments:
In
`@src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java`:
- Around line 38-48: The handler handleRetryableException for
RetryableHttpException is never reached because ChatService.fetchResponseFromLLM
is wrapped with `@CircuitBreaker` which calls the fallback method instead; either
remove or change the circuit breaker fallback so the exception is propagated
(e.g., remove the fallback attribute or have the fallback rethrow or
wrap/propagate RetryableHttpException) so handleRetryableException can execute,
or if the fallback behavior is intended, delete handleRetryableException to
avoid dead code — reference ChatService.fetchResponseFromLLM, the circuit
breaker fallback method named fallback, and
GlobalExceptionHandler.handleRetryableException when making the change.

In `@src/main/java/org/example/projectbifrost/service/ChatService.java`:
- Around line 75-78: The current lambda throws a generic
RetryableHttpException("The Gods are silent") and discards upstream HTTP
details; update the flow so RetryableHttpException carries the original HTTP
status, optional Retry-After header and a truncated response body excerpt
(similar to InvalidLLMResponseException's statusCode pattern). Change the lambda
inside ChatService (the .onStatus handler) to extract resp.status().value(),
resp.headers().asHttpHeaders().getFirst("Retry-After") and a safe, truncated
resp.bodyAsString() (or buffer peek) and construct a new
RetryableHttpException(statusCode, retryAfter, bodyExcerpt, message). Modify the
RetryableHttpException class to add fields (int statusCode, String retryAfter,
String bodyExcerpt), constructors and getters, and update
GlobalExceptionHandler.handleRetryableException to log and expose these fields
(and make retry logic branch on statusCode==429 using retryAfter) so
logs/metrics can distinguish rate limits from server errors.
- Around line 92-94: The fallback in ChatService.fallback currently swallows
Exception e and returns a literal "Fallback!" which hides errors and prevents
GlobalExceptionHandler.handleRetryableException from handling
RetryableHttpException; update fallback to either (A) rethrow the original
exception (or wrap and throw a RetryableHttpException) so chatWithLLM lets the
error propagate to GlobalExceptionHandler, removing the fallbackMethod if
propagation is desired, or (B) if a persisted sentinel is required, log the full
exception with context via the existing logger, increment/error metrics, and
return a clear sentinel string (e.g., "<LLM_ERROR>") while ensuring the
controller can still map that sentinel to a 503; refer to ChatService.fallback,
chatWithLLM, GlobalExceptionHandler.handleRetryableException, and
RetryableHttpException when making the change.

---

Nitpick comments:
In
`@src/main/java/org/example/projectbifrost/configuration/RestClientConfiguration.java`:
- Around line 8-14: Remove the unused Spring annotation and import: the
`@EnableResilientMethods` import and annotation on RestClientConfiguration are not
needed because the project uses Resilience4j annotations (`@CircuitBreaker`,
`@Retry`) wired by resilience4j-spring-boot3; open the RestClientConfiguration
class, delete the import for
org.springframework.resilience.annotation.EnableResilientMethods and remove the
`@EnableResilientMethods` annotation declaration so only needed imports and the
`@Configuration` remain.

In `@src/main/java/org/example/projectbifrost/service/ChatService.java`:
- Around line 36-45: Update the Javadoc for ChatService.sendChatRequest to
accurately reflect all exceptions and behaviors: mention that
fetchResponseFromLLM may throw RetryableHttpException and other
RestClientException types in addition to InvalidLLMResponseException, and note
any fallback string behavior (e.g., returning "Fallback!" if applicable) until
the proxy/fallback design is finalized; reference the sendChatRequest method,
fetchResponseFromLLM, RetryableHttpException, RestClientException, and
InvalidLLMResponseException so callers know what to expect and when to handle
retries or fallbacks.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0bbe1f73-77f9-4584-b670-e723ba5f48fc

📥 Commits

Reviewing files that changed from the base of the PR and between f6d860e and 01b1e40.

📒 Files selected for processing (8)
  • pom.xml
  • src/main/java/org/example/projectbifrost/configuration/RestClientConfiguration.java
  • src/main/java/org/example/projectbifrost/exception/GlobalExceptionHandler.java
  • src/main/java/org/example/projectbifrost/exception/InvalidLLMResponseException.java
  • src/main/java/org/example/projectbifrost/exception/RetryableHttpException.java
  • src/main/java/org/example/projectbifrost/service/ChatService.java
  • src/main/resources/application.properties
  • src/test/java/org/example/projectbifrost/service/ChatServiceTest.java
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/org/example/projectbifrost/exception/RetryableHttpException.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/test/java/org/example/projectbifrost/service/ChatServiceTest.java
  • pom.xml
  • src/main/resources/application.properties

Comment thread src/main/java/org/example/projectbifrost/service/ChatService.java
Comment thread src/main/java/org/example/projectbifrost/service/ChatService.java Outdated
…n with exponential backoff and custom exceptions. Enhance fallback handling with detailed logging.

@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: 1

🧹 Nitpick comments (2)
src/main/resources/application.properties (1)

17-18: 💤 Low value

Confirm the aspect-order inversion is intentional.

You set circuitBreakerAspectOrder=1 and retryAspectOrder=2, which (in Spring AOP, lower order = outer) makes the effective wrapping CircuitBreaker( Retry( method ) ). Resilience4j's documented default is the opposite: "The Resilience4j Aspects order is the following: Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ) ) ) so Retry is applied at the end (if needed)."

Practical consequences of the PR's order:

  • When the circuit is OPEN, the call short-circuits to fallback immediately — Retry never runs (often desirable).
  • A logical call that exhausts all retries counts as one CB failure event, not three (less aggressive CB tripping).
  • Conversely, transient failures recovered by Retry are completely invisible to the CB sliding window, so the CB can't react to a noisy-but-recovering upstream.

If you intended the default semantics (each individual attempt counted by the CB, retries run even when the circuit was closed at the first attempt), swap the orders. Otherwise, a one-line comment in the properties file documenting the intent would help future readers.

🤖 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/main/resources/application.properties` around lines 17 - 18, Current
aspect order sets circuitBreakerAspectOrder=1 and retryAspectOrder=2, which
makes CircuitBreaker wrap Retry (CircuitBreaker( Retry(method) )); either swap
the two properties to restore Resilience4j's documented default Retry
outer-of-CircuitBreaker (set retryAspectOrder=1 and circuitBreakerAspectOrder=2)
or, if the inversion is intentional, add a one-line comment next to
resilience4j.circuitbreaker.circuitBreakerAspectOrder and
resilience4j.retry.retryAspectOrder explaining the deliberate semantics (e.g.,
"Intentional: CircuitBreaker outer of Retry to short-circuit OPEN circuits and
avoid counting retries as multiple CB events").
src/main/java/org/example/projectbifrost/service/ChatService.java (1)

97-97: 💤 Low value

Consider matching the fallback method signature to be more explicit and robust against future additions.

The current fallback(Exception e) works today because fetchResponseFromLLM has exactly one parameter (Resilience4j's global fallback pattern matches this case). However, if you later add other @CircuitBreaker(name="chatService", fallbackMethod="fallback") methods with different parameter shapes, this fallback won't match them.

Making the signature explicit—public String fallback(List<OpenRouterRequestDTO.Message> apiMessages, Exception e)—is more conventional and defensive, giving the fallback access to request context and ensuring it works correctly regardless of future method additions.

🤖 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/main/java/org/example/projectbifrost/service/ChatService.java` at line
97, Change the fallback method signature in ChatService from public String
fallback(Exception e) to public String
fallback(List<OpenRouterRequestDTO.Message> apiMessages, Exception e) so it
matches Resilience4j's expected fallback shape for fetchResponseFromLLM and
future `@CircuitBreaker` methods; update the method body to use the passed
apiMessages instead of relying solely on external state and ensure any
references to fallback in the class still resolve to the new signature (method
name: fallback, related method: fetchResponseFromLLM).
🤖 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.

Inline comments:
In `@src/main/java/org/example/projectbifrost/service/ChatService.java`:
- Around line 59-64: The current flow persists the literal fallback string into
history because fetchResponseFromLLM can return the sentinel "Fallback!" and
chatSession.addMessage(new ChatMessage("assistant", content)) always runs;
change the logic so fallback does not become stored assistant content by either
(preferred) making fallback throw a dedicated LLMUnavailableException (e.g.,
from fetchResponseFromLLM) and let
GlobalExceptionHandler.handleRetryableException handle it, or (alternative) have
fetchResponseFromLLM return Optional<String> / null and in chatWithLLM only call
chatSession.addMessage for the assistant when a real response is present (i.e.,
skip persisting when the sentinel/empty Optional is returned); update references
to fetchResponseFromLLM, fallback, chatSession.addMessage, and
GlobalExceptionHandler.handleRetryableException accordingly.

---

Nitpick comments:
In `@src/main/java/org/example/projectbifrost/service/ChatService.java`:
- Line 97: Change the fallback method signature in ChatService from public
String fallback(Exception e) to public String
fallback(List<OpenRouterRequestDTO.Message> apiMessages, Exception e) so it
matches Resilience4j's expected fallback shape for fetchResponseFromLLM and
future `@CircuitBreaker` methods; update the method body to use the passed
apiMessages instead of relying solely on external state and ensure any
references to fallback in the class still resolve to the new signature (method
name: fallback, related method: fetchResponseFromLLM).

In `@src/main/resources/application.properties`:
- Around line 17-18: Current aspect order sets circuitBreakerAspectOrder=1 and
retryAspectOrder=2, which makes CircuitBreaker wrap Retry (CircuitBreaker(
Retry(method) )); either swap the two properties to restore Resilience4j's
documented default Retry outer-of-CircuitBreaker (set retryAspectOrder=1 and
circuitBreakerAspectOrder=2) or, if the inversion is intentional, add a one-line
comment next to resilience4j.circuitbreaker.circuitBreakerAspectOrder and
resilience4j.retry.retryAspectOrder explaining the deliberate semantics (e.g.,
"Intentional: CircuitBreaker outer of Retry to short-circuit OPEN circuits and
avoid counting retries as multiple CB events").
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b95978e7-a835-4cb1-92e3-a8b6b1b6cf35

📥 Commits

Reviewing files that changed from the base of the PR and between 01b1e40 and 96925bc.

📒 Files selected for processing (2)
  • src/main/java/org/example/projectbifrost/service/ChatService.java
  • src/main/resources/application.properties

Comment thread src/main/java/org/example/projectbifrost/service/ChatService.java
@codebyNorthsteep
codebyNorthsteep merged commit 216a45d into main May 8, 2026
1 check was pending
@coderabbitai coderabbitai Bot mentioned this pull request May 8, 2026
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