Skip to content

fix(client): make retry backoff abortable via AbortSignal - #2653

Open
GISWLH wants to merge 2 commits into
openai:mainfrom
GISWLH:fix/abortable-retry-backoff
Open

fix(client): make retry backoff abortable via AbortSignal#2653
GISWLH wants to merge 2 commits into
openai:mainfrom
GISWLH:fix/abortable-retry-backoff

Conversation

@GISWLH

@GISWLH GISWLH commented Sep 6, 2026

Copy link
Copy Markdown

Summary

  • Make sleep accept optional AbortSignals and clear its timer on abort.
  • Pass the request signal(s) through retryRequest backoff so abort during Retry-After / default backoff settles promptly instead of waiting the full delay.
  • Preserve abort cause via APIUserAbortError.

Fixes #2580.

Test plan

  • vitest run --config vitest.config.mts tests/internal/utils.test.ts tests/client-behavior.test.ts (74 passed)

Pass request AbortSignals through sleep during Retry-After backoff so
abort settles promptly instead of waiting out the full delay. Add unit
tests for mid-backoff abort. Fixes openai#2580.
@GISWLH
GISWLH requested a review from a team as a code owner September 6, 2026 03:19

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 224f8feab5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/client.ts Outdated
} else {
await sleep(timeoutMillis);
const retrySignals =
requestSignal === options.signal ? [requestSignal] : [requestSignal, options.signal];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prioritize the caller signal when both signals abort

When a protected/provider hook replaces req.signal with a signal that is synchronously aborted in response to the original options.signal, both signals are aborted before this catch resumes. Because retrySignals puts the replacement first, find() selects it and _makeUserAbortError exposes its reason rather than the caller's reason, unlike the caller-first precedence used for active requests at lines 1255-1257 and 1278-1279. Put options.signal first (while still listening to both) so abort-cause preservation remains consistent during backoff.

AGENTS.md reference: AGENTS.md:L119-L123

Useful? React with 👍 / 👎.

Comment thread src/internal/utils/sleep.ts Outdated
settle(() => reject(error));
}
}
if (activeSignals.some((signal) => signal.aborted)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clean up when the post-registration abort probe throws

If a structural or polyfilled signal's aborted getter succeeds before listener registration but throws during this final race-closing probe, the Promise constructor converts that throw directly into a rejection without calling settle(). The retry timer and all installed abort listeners consequently remain live until the backoff expires (up to 60 seconds in this client), retaining request state after the operation has already failed. Catch failures from this probe and reject through settle() so the timer and listeners are cleaned up.

AGENTS.md reference: AGENTS.md:L119-L123

Useful? React with 👍 / 👎.

break;
}
try {
signal.addEventListener('abort', abort, { once: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove listeners installed after synchronous cancellation

If a structural or polyfilled signal invokes abort synchronously from addEventListener() and then finishes installing that listener, settle() runs its cleanup before the listener has actually been registered. The loop then observes settled and exits without removing the newly installed listener; since the signal has already aborted, { once: true } may never clean it up, permanently retaining the retry closure. Check settled immediately after registration and explicitly remove the just-added listener, as the repository's other abortable wait implementations do.

AGENTS.md reference: AGENTS.md:L119-L123

Useful? React with 👍 / 👎.

Prefer the caller AbortSignal reason when both signals abort during
backoff, and harden sleep cleanup for sync registration and probe
failures so timers/listeners do not leak.
@GISWLH

GISWLH commented Sep 6, 2026

Copy link
Copy Markdown
Author

Addressed the 3 P2 Codex review notes in 42067d7:

  1. Prefer caller options.signal when selecting abort reason during retry backoff.
  2. Route post-registration aborted probe failures through settle() so timers/listeners are cleaned up.
  3. Remove listeners installed after synchronous cancellation during addEventListener.

npx vitest run tests/internal/utils.test.ts tests/client-behavior.test.ts — 77 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.

Abort during retry backoff waits for the full delay

1 participant