Skip to content

fix: retry synchronous failures from async delegates - #891

Open
sylvesterkaczmarek wants to merge 2 commits into
openai:mainfrom
sylvesterkaczmarek:fix/async-retry-sync-delegate-failure
Open

fix: retry synchronous failures from async delegates#891
sylvesterkaczmarek wants to merge 2 commits into
openai:mainfrom
sylvesterkaczmarek:fix/async-retry-sync-delegate-failure

Conversation

@sylvesterkaczmarek

Copy link
Copy Markdown

Summary

Route synchronous exceptions thrown by HttpClient.executeAsync() through RetryingHttpClient's existing async retry machinery instead of letting them escape before retry handling is attached.

Fixes #888.

Problem

RetryingHttpClient.executeAsync() currently calls its delegate directly:

val responseFuture = httpClient.executeAsync(requestWithRetryCount, requestOptions)

A custom HttpClient can throw synchronously from executeAsync() rather than returning an exceptionally completed future. In that case, the exception escapes before the existing handleAsync retry path runs.

That means synchronous IOException, OpenAIIoException, and OpenAIRetryableException failures bypass maxRetries entirely, even though the same failures are retryable when delivered asynchronously.

Fix

Wrap the delegate call and convert a synchronous throwable into an exceptionally completed CompletableFuture<HttpResponse>.

The resulting future then flows through the existing retry handler unchanged, so:

  • retryable synchronous failures use the normal backoff and retry-count logic;
  • non-retryable synchronous failures are returned through the async API as failed futures;
  • no duplicate retry policy is introduced.

Regression coverage

Added focused tests for:

  1. a synchronous OpenAIRetryableException followed by a successful response, verifying two attempts and retry-count headers 0 then 1;
  2. a synchronous non-retryable exception, verifying executeAsync() itself returns normally with an exceptionally completed future and no retry/backoff occurs.

Validation

  • branch is based on current upstream main at cf942a40074291290634321ad9fe21e514030b4c;
  • branch is 0 commits behind upstream;
  • production diff is 8 additions / 1 deletion in RetryingHttpClient.kt;
  • retry classification, backoff, response handling, and synchronous execute() behavior are unchanged.

Full repository validation is left to GitHub Actions.

Risk

Low. Existing delegates that already return futures are unaffected. The behavior changes only when an async delegate throws before returning a future.

@sylvesterkaczmarek
sylvesterkaczmarek requested a review from a team as a code owner August 18, 2026 15:56
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.

RetryingHttpClient.executeAsync skips retry handling when delegate throws synchronously

1 participant