Skip to content

feat(typesafe): add jittered exponential backoff retries for transient transport errors - #4591

Open
AseemPrasad wants to merge 1 commit into
ComposioHQ:nextfrom
AseemPrasad:aseemone
Open

AseemPrasad wants to merge 1 commit into
ComposioHQ:nextfrom
AseemPrasad:aseemone

Conversation

@AseemPrasad

@AseemPrasad AseemPrasad commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds configurable exponential backoff retries with full jitter to @composio/providers/typesafe transport requests (createAsk). Automatically recovers from transient HTTP rate-limiting (429), gateway timeouts, and server errors (500, 502, 503, 504) without throwing premature errors during agent runs.

Key Changes

Type Definitions (ts/packages/providers/typesafe/src/types.ts):
Added optional maxRetries?: number (Default: 3).
Added optional backoffMs?: number (Default: 200).
Added optional retryStatusCodes?: number[] (Default: [429, 500, 502, 503, 504]).
Transport Execution Loop (ts/packages/providers/typesafe/src/decide.ts):
Implemented exponential backoff with randomized jitter in createAsk: $$\text{delay} = \min(\text{backoffMs} \times 2^{\text{attempt}}, 5000) \times (0.8 + 0.4 \times \text{Math.random()})$$
Distinguishes retryable errors (rate_limit, server_error, connection, timeout, or status in retryStatusCodes) from unretryable failures (aborted, authentication errors, 400 Bad Requests).
Short-circuits immediately if requestOptions.signal.aborted is triggered.

Unit Test Suite (ts/packages/providers/typesafe/test/transport.test.ts):

Added test case verifying recovery after transient 429 rate limit.
Added test case verifying retry exhaustion when 503 persistence failures occur.

@vercel

vercel Bot commented Sep 22, 2026

Copy link
Copy Markdown

AseemPrasad is attempting to deploy a commit to the Composio Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

The PR is not yet safe to merge because cancellation can remain blocked during backoff and configured status-code exclusions are ignored.

Findings

  1. P1 Abort waits through backoff ▶
  2. P1 Status exclusions are ignored ▶
  3. P2 Malformed responses count twice ▶

Summary

This PR adds configurable, jittered exponential-backoff retries to the typesafe provider transport and adds coverage for recovery and retry exhaustion.

  • Retries classified transient transport failures with configurable attempt counts, delays, and status codes.
  • Preserves request signals and per-attempt timeouts.
  • Adds tests for a transient 429 followed by success and persistent 503 failures.
  • Cancellation during backoff, status-code opt-outs, and malformed-response request accounting need correction.
Diagram
sequenceDiagram
  participant Agent
  participant Provider as Typesafe Provider
  participant API as Typesafe API
  Agent->>Provider: decide(options, signal)
  Provider->>API: systemOne()
  API-->>Provider: transient error
  Provider->>Provider: classify retryability
  Provider->>Provider: jittered backoff
  alt signal aborts during backoff
    Note over Provider: Current timer remains pending
    Provider-->>Agent: aborted only after delay
  else retry proceeds
    Provider->>API: systemOne()
    API-->>Provider: response
    Provider->>Provider: validate answers
    Provider-->>Agent: decision and request metadata
  end
Loading

Reviews (1) · Last reviewed commit: "feat(typesafe): add jittered exponential..."

const jitter = 0.8 + 0.4 * Math.random();
const delayMs = Math.round(baseDelay * jitter);

await new Promise(resolve => setTimeout(resolve, delayMs));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Abort waits through backoff

If the signal is aborted while this backoff timer is pending, the timer continues for the entire jittered delay before the loop observes the abort. With a configured backoff at the cap, cancellation can therefore be delayed by as much as six seconds, contradicting the request's immediate cancellation behavior. Make the delay abort-aware and clear its timer when the signal fires.

Comment on lines +225 to +230
const isRetryable =
providerError.reason === 'rate_limit' ||
providerError.reason === 'server_error' ||
providerError.reason === 'connection' ||
providerError.reason === 'timeout' ||
(providerError.status !== undefined && retryStatusCodes.has(providerError.status));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Status exclusions are ignored

retryStatusCodes cannot disable any default status: 429 is always retried through rate_limit, while every 5xx response is always retried through server_error, independently of membership in the configured set. Consequently, callers passing [] or removing a default code still receive retries despite the public option describing the configured codes as those that trigger retries.

return { ...validateAnswers(data, questions, requestId), requestId };
} catch (error) {
onRequest?.(undefined);
const providerError = toProviderError(error);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Malformed responses count twice

A malformed successful response now invokes onRequest twice: once with its request ID before validation and again with undefined when validateAnswers throws inside this catch. In fan-out paths where another request succeeds, the returned metadata consequently counts one malformed request twice, making meta.requestCount inaccurate.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 97c9a17. Configure here.

const jitter = 0.8 + 0.4 * Math.random();
const delayMs = Math.round(baseDelay * jitter);

await new Promise(resolve => setTimeout(resolve, delayMs));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Retries ignore server retry delay

Medium Severity

toProviderError already preserves retryAfterMs on rate_limit errors, but the new retry loop always waits the local exponential backoff instead. When the API asks for a longer delay, the extra attempts land inside the same window, exhaust maxRetries, and still fail, so 429s this change is meant to absorb can surface as hard errors.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 97c9a17. Configure here.

const jitter = 0.8 + 0.4 * Math.random();
const delayMs = Math.round(baseDelay * jitter);

await new Promise(resolve => setTimeout(resolve, delayMs));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Backoff delay ignores abort signal

Medium Severity

After a retryable failure the wait uses a raw setTimeout and does not listen to requestOptions.signal. An abort during that wait stays pending until the timer fires, so cancellation is delayed by up to several seconds once backoff reaches the 5s cap.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 97c9a17. Configure here.

@AseemPrasad

Copy link
Copy Markdown
Contributor Author

Alberto Schiabel (@jkomyno) great to have the previous prs reviewed and incorporated..
would love to get this one reviewed as well..
thank you..

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