Skip to content

fix: stop retrying Exa requests after a tool times out - #473

Open
Mohith26 wants to merge 1 commit into
exa-labs:mainfrom
Mohith26:fix/stop-retries-after-timeout
Open

Mohith26 wants to merge 1 commit into
exa-labs:mainfrom
Mohith26:fix/stop-retries-after-timeout

Conversation

@Mohith26

@Mohith26 Mohith26 commented Nov 3, 2026

Copy link
Copy Markdown

withTimeout rejects the tool call when the timeout fires, but the retryWithBackoff loop inside it keeps going. If an attempt fails with a transient status after the timeout (for example a 504 that comes back at ~60s), the loop waits out the backoff and sends another /search or /contents request, even though the MCP client already got the "timed out" error. With the default of 2 retries, one timed-out call can send up to two extra requests that nobody reads.

This change:

  • withTimeout now passes an AbortSignal to the wrapped function and aborts it when the timeout fires.
  • retryOnTransient / retryWithBackoff take an optional signal. Once it is aborted they stop scheduling attempts (the backoff wait also ends early) and rethrow the last error.
  • web_search_exa, web_fetch_exa and web_search_advanced_exa pass the signal through.

Existing callers that don't pass a signal behave exactly as before. The in-flight request itself is not cancelled; doing that would need the exa-js bump to a version with per-call signal support (2.26+), which I left out to keep this small.

Testing: added two cases to tests/unit/utils/errorHandler.test.ts. The first wraps a function that fails with a 504 after 40s in a 60s withTimeout; on main it is called a third time after the timeout, with this change it stops at two. Both new tests fail on main and pass here. npm run ci (prettier check, typecheck, vitest: 255 tests) passes.

AI-assisted; tested locally with npm run ci.

withTimeout rejected the tool call but left retryWithBackoff running, so a slow transient failure (for example a 504 that lands after the timeout) still triggered new requests that nobody was waiting for. withTimeout now passes an AbortSignal that aborts when it fires, and retryWithBackoff stops scheduling attempts once that signal is aborted.

This branch has not been deployed

No deployments
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