Skip to content

fix(core): add a default timeout for outbound HTTP requests - #614

Merged
devjain32 merged 5 commits into
corsairdev:mainfrom
yuvanvk:fix/586-core
Aug 6, 2026
Merged

fix(core): add a default timeout for outbound HTTP requests#614
devjain32 merged 5 commits into
corsairdev:mainfrom
yuvanvk:fix/586-core

Conversation

@yuvanvk

@yuvanvk yuvanvk commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Description

Adds a default wall-clock timeout to Corsair’s shared HTTP request path.

Previously, sendRequest created an AbortController, but fetch requests were only aborted when callers explicitly cancelled. A hung upstream fetch could keep the promise pending indefinitely. This change combines the existing cancellation signal with a timeout signal so requests abort automatically after the configured timeout while preserving caller cancellation.

Changes include:

  • Adding optional TIMEOUT support to OpenAPIConfig
  • Applying a default request timeout in sendRequest
  • Combining caller cancellation with timeout-based abort handling
  • Adding unit coverage with mocked hanging fetch calls for both timeout aborts and explicit caller cancellation

Fixes: #586

Checklist

Before submitting your PR, please verify the following:

  • I have run pnpm lint and all checks pass
  • I have run pnpm build and all packages build successfully
  • I have added or updated tests where applicable

Screenshots / Demos (if applicable)

Screenshot 2026-08-05 at 4 00 18 PM

Additional Notes

Verified the focused test with:

pnpm --filter corsair test -- request-timeout.test.ts

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added an optional configurable request timeout setting (TIMEOUT) with a 20-second default.
  • Bug Fixes

    • Improved request timeout behavior so requests are automatically aborted when the limit is reached, while preserving existing manual cancellation behavior.
  • Tests

    • Added automated coverage for timeout aborts and caller-initiated cancellations, including signal handling and fetch mocking.

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@yuvanvk is attempting to deploy a commit to the corsair Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the core Changes in packages/corsair label Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The request configuration adds an optional timeout. sendRequest defaults to 20 seconds and combines timeout cancellation with caller cancellation. Tests cover timeout aborts and promise.cancel() behavior.

Changes

Request timeout handling

Layer / File(s) Summary
Timeout configuration and request cancellation
packages/corsair/async-core/OpenAPI.ts, packages/corsair/async-core/request.ts
OpenAPIConfig adds optional TIMEOUT. sendRequest uses this value, defaults to 20,000 ms, and combines timeout and caller abort signals.
Timeout and cancellation tests
packages/corsair/tests/request-timeout.test.ts
Tests verify timeout-triggered aborts, caller cancellation, rejection behavior, and restoration of global.fetch.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant sendRequest
  participant AbortSignal
  participant fetch
  Caller->>sendRequest: Start request with TIMEOUT
  sendRequest->>AbortSignal: Combine timeout and caller signals
  sendRequest->>fetch: Send request with combined signal
  AbortSignal-->>fetch: Abort on timeout or caller cancellation
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a default timeout for HTTP requests in the core module.
Linked Issues check ✅ Passed The PR satisfies all requirements from issue #586: adds configurable timeout via OpenAPIConfig, implements timeout abort with AbortSignal.any, preserves caller cancellation, and includes unit tests for both timeout and cancellation paths.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #586 requirements: OpenAPIConfig timeout property, sendRequest timeout implementation, and comprehensive request-timeout tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@yuvanvk yuvanvk changed the title Fix/586 core fix(core): add a default timeout for outbound HTTP requests Aug 5, 2026
@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a configurable wall-clock timeout to Corsair’s shared outbound HTTP path while retaining explicit caller cancellation.

  • Extends OpenAPIConfig with an optional TIMEOUT.
  • Combines timeout and cancellation signals for outbound fetches, defaulting to 20 seconds.
  • Adds tests for timeout-triggered aborts and explicit cancellation.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/corsair/async-core/OpenAPI.ts Adds the optional timeout field to the shared HTTP configuration type.
packages/corsair/async-core/request.ts Applies a 20-second default and combines timeout-based aborts with existing caller cancellation.
packages/corsair/tests/request-timeout.test.ts Covers timeout-triggered aborts and preservation of explicit cancellation behavior.

Sequence Diagram

sequenceDiagram
  participant Caller
  participant Request as request()
  participant Fetch
  Caller->>Request: Start outbound request
  Request->>Fetch: "fetch(signal = cancellation OR timeout)"
  alt Caller cancels
    Caller->>Request: cancel()
    Request-->>Fetch: Abort signal
    Request-->>Caller: CancelError
  else Timeout expires
    Request-->>Fetch: Timeout abort signal
    Fetch-->>Caller: Request rejected
  else Response arrives
    Fetch-->>Request: Response
    Request-->>Caller: Parsed result
  end
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into fix/586-core" | Re-trigger Greptile

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

🧹 Nitpick comments (1)
packages/corsair/async-core/request.ts (1)

234-234: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Declare the minimum Node.js version. The repository requires Node.js 22+, and the Node build supports both APIs. packages/corsair/package.json has no engines field. Add "engines": { "node": ">=22" }, or provide a fallback if older runtimes are supported.

🤖 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 `@packages/corsair/async-core/request.ts` at line 234, Add an engines
declaration to packages/corsair/package.json requiring Node.js >=22, matching
the AbortSignal.any usage in the request implementation. Do not add a fallback
unless this package must support older Node.js runtimes.
🤖 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 `@packages/corsair/async-core/request.ts`:
- Line 234: Update the request timeout handling around the AbortSignal
construction in the request flow to enforce config.TIMEOUT using elapsed
wall-clock time rather than relying solely on AbortSignal.timeout, which pauses
during suspension or bfcache. Preserve cancellation through controller.signal
and ensure the request aborts once the real elapsed duration reaches the
configured timeout.

In `@packages/corsair/tests/request-timeout.test.ts`:
- Around line 59-68: Update the caller-cancellation test around request(config,
options) to use a separate request configuration with a sufficiently long
timeout, ensuring the shared 5 ms timeout cannot fire before promise.cancel()
executes. Preserve the existing wait for the abort signal and the isCancelled
rejection assertion.

---

Nitpick comments:
In `@packages/corsair/async-core/request.ts`:
- Line 234: Add an engines declaration to packages/corsair/package.json
requiring Node.js >=22, matching the AbortSignal.any usage in the request
implementation. Do not add a fallback unless this package must support older
Node.js runtimes.
🪄 Autofix

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 Plus

Run ID: da3a5f97-5cbe-4133-a50f-e077ace0ce64

📥 Commits

Reviewing files that changed from the base of the PR and between 9222086 and 8de13b9.

📒 Files selected for processing (3)
  • packages/corsair/async-core/OpenAPI.ts
  • packages/corsair/async-core/request.ts
  • packages/corsair/tests/request-timeout.test.ts

body: body ?? formData,
method: options.method,
signal: controller.signal,
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(timeout)]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

According to the WHATWG DOM specification or MDN, does AbortSignal.timeout() measure active time or elapsed wall-clock time when a document is suspended or in the back-forward cache?

💡 Result:

Yes, according to MDN and the underlying design of the WHATWG DOM specification, AbortSignal.timeout measures active time rather than wall-clock time [1][2]. When a document is suspended in the back-forward cache (bfcache) or when code is running in a suspended worker, the timer effectively pauses [1][2]. This means that the time spent in these suspended states does not count toward the timeout duration [3]. Consequently, if you set a timeout of 10 seconds, the signal will abort only after the document or worker has spent a total of 10 seconds in an active, non-suspended state [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- request.ts structure ---'
ast-grep outline packages/corsair/async-core/request.ts --view compact
printf '%s\n' '--- request.ts relevant lines ---'
sed -n '190,255p' packages/corsair/async-core/request.ts
printf '%s\n' '--- timeout references ---'
rg -n --glob '!node_modules' 'TIMEOUT|AbortSignal\.(timeout|any)|wall.?clock|active.?time|timeout' packages/corsair package.json packages 2>/dev/null | head -200
printf '%s\n' '--- repository support configuration ---'
fd -HI -t f '(package\.json|tsconfig.*|browserslist|\.nvmrc|README.*|CHANGELOG.*)' . | head -200

Repository: corsairdev/corsair

Length of output: 336


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- request.ts structure ---'
ast-grep outline packages/corsair/async-core/request.ts --view signatures
printf '%s\n' '--- request.ts relevant lines ---'
sed -n '190,255p' packages/corsair/async-core/request.ts
printf '%s\n' '--- timeout references ---'
rg -n --glob '!node_modules' 'TIMEOUT|AbortSignal\.(timeout|any)|wall.?clock|active.?time|timeout' packages/corsair package.json packages 2>/dev/null | head -200
printf '%s\n' '--- repository support configuration ---'
fd -HI -t f '(package\.json|tsconfig.*|browserslist|\.nvmrc|README.*|CHANGELOG.*)' . | head -200

Repository: corsairdev/corsair

Length of output: 28137


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- package and runtime targets ---'
cat package.json
cat packages/corsair/package.json
cat .nvmrc
printf '%s\n' '--- TypeScript targets ---'
cat packages/corsair/tsconfig.json
cat packages/corsair/tsconfig.build.json
printf '%s\n' '--- timeout test ---'
cat -n packages/corsair/tests/request-timeout.test.ts
printf '%s\n' '--- request call path ---'
sed -n '320,390p' packages/corsair/async-core/request.ts

Repository: corsairdev/corsair

Length of output: 10533


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- current change summary ---'
git status --short
git diff --stat
printf '%s\n' '--- relevant diff ---'
git diff -- packages/corsair/async-core/request.ts packages/corsair/tests/request-timeout.test.ts
printf '%s\n' '--- base compiler options ---'
cat tsconfig.base.json
printf '%s\n' '--- browser and client references ---'
rg -n 'async-core|CancelablePromise|OpenAPIConfig|request\(' packages/corsair/README.md packages/corsair/client packages/corsair --glob '!tests/**' | head -200

Repository: corsairdev/corsair

Length of output: 6021


Implement a wall-clock timeout for requests. AbortSignal.timeout(timeout) measures active time. A suspended document or bfcache entry can therefore exceed config.TIMEOUT before the signal aborts. Use an elapsed-time strategy, or document active-time semantics.

🤖 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 `@packages/corsair/async-core/request.ts` at line 234, Update the request
timeout handling around the AbortSignal construction in the request flow to
enforce config.TIMEOUT using elapsed wall-clock time rather than relying solely
on AbortSignal.timeout, which pauses during suspension or bfcache. Preserve
cancellation through controller.signal and ensure the request aborts once the
real elapsed duration reaches the configured timeout.

Comment on lines +59 to +68
const promise = request(config, options);

// Wait until fetch has started and has received its abort signal
while (!signal) {
await new Promise((res) => setTimeout(res, 0));
}

promise.cancel();

await expect(promise).rejects.toMatchObject({ isCancelled: true });

@coderabbitai coderabbitai Bot Aug 5, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a long timeout in the caller-cancellation test.

The shared 5 ms timeout starts before fetch() receives its signal. If the scheduler delays this test, the timeout can reject the promise before Line 66 calls promise.cancel(). Use a separate config with a long timeout so this test only verifies caller cancellation.

Proposed fix
-		const promise = request(config, options);
+		const promise = request({ ...config, TIMEOUT: 60_000 }, options);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const promise = request(config, options);
// Wait until fetch has started and has received its abort signal
while (!signal) {
await new Promise((res) => setTimeout(res, 0));
}
promise.cancel();
await expect(promise).rejects.toMatchObject({ isCancelled: true });
const promise = request({ ...config, TIMEOUT: 60_000 }, options);
// Wait until fetch has started and has received its abort signal
while (!signal) {
await new Promise((res) => setTimeout(res, 0));
}
promise.cancel();
await expect(promise).rejects.toMatchObject({ isCancelled: true });
🤖 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 `@packages/corsair/tests/request-timeout.test.ts` around lines 59 - 68, Update
the caller-cancellation test around request(config, options) to use a separate
request configuration with a sufficiently long timeout, ensuring the shared 5 ms
timeout cannot fire before promise.cancel() executes. Preserve the existing wait
for the abort signal and the isCancelled rejection assertion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

2000ms is good right instead of 5ms?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@yuvanvk, yes. 2000ms is sufficient for this test and removes the practical race caused by 5ms.

A separate longer timeout is still preferable because it keeps the caller-cancellation test independent from the timeout path. However, 2000ms is a reasonable value if it is the configured default. The passing focused test output supports this change.

You are interacting with an AI system.

@Dhirenderchoudhary

Copy link
Copy Markdown
Collaborator

Core idea LGTM TIMEOUT ?? 20_000 + AbortSignal.any([controller, AbortSignal.timeout(...)]) is the right fix; cancel path still wired.
@yuvanvk
One gap:

  • Cancel test is flaky shared TIMEOUT: 5 (5ms). Give the cancel case a long timeout so promise.cancel() can’t lose the race to the timeout abort.

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

🧹 Nitpick comments (1)
packages/corsair/tests/request-timeout.test.ts (1)

12-12: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use separate timeout values for each test.

TIMEOUT: 2_000 makes the timeout test wait about two seconds. Keep a short timeout for the timeout test. Use a long timeout only in the caller-cancellation test. This prevents the cancellation race without slowing the focused test.

Proposed change
-	TIMEOUT: 2_000,
+	TIMEOUT: 50,
-		const promise = request(config, options);
+		const promise = request({ ...config, TIMEOUT: 60_000 }, options);
🤖 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 `@packages/corsair/tests/request-timeout.test.ts` at line 12, Update the
timeout test setup in request-timeout.test.ts so each test uses its own timeout
value instead of sharing TIMEOUT: 2_000. Keep the focused timeout test on a
short timeout, and apply the longer timeout only in the caller-cancellation test
path to avoid the race without slowing the other test. Use the existing
test-case symbols in this file to separate the per-test timeout configuration
cleanly.
🤖 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.

Nitpick comments:
In `@packages/corsair/tests/request-timeout.test.ts`:
- Line 12: Update the timeout test setup in request-timeout.test.ts so each test
uses its own timeout value instead of sharing TIMEOUT: 2_000. Keep the focused
timeout test on a short timeout, and apply the longer timeout only in the
caller-cancellation test path to avoid the race without slowing the other test.
Use the existing test-case symbols in this file to separate the per-test timeout
configuration cleanly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1da37deb-d00a-451a-8f8f-a8aa0009dd49

📥 Commits

Reviewing files that changed from the base of the PR and between 8de13b9 and b447098.

📒 Files selected for processing (1)
  • packages/corsair/tests/request-timeout.test.ts

@yuvanvk

yuvanvk commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Core idea LGTM TIMEOUT ?? 20_000 + AbortSignal.any([controller, AbortSignal.timeout(...)]) is the right fix; cancel path still wired. @yuvanvk One gap:

  • Cancel test is flaky shared TIMEOUT: 5 (5ms). Give the cancel case a long timeout so promise.cancel() can’t lose the race to the timeout abort.

@Dhirenderchoudhary changed TIMEOUT from 5ms to 2 seconds

@ambikeesshh

Copy link
Copy Markdown
Collaborator

@greptileai review

@ambikeesshh ambikeesshh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm now. thanks @yuvanvk

@devjain32
devjain32 merged commit 04941a2 into corsairdev:main Aug 6, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Changes in packages/corsair

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(core): add a default timeout for outbound HTTP requests

4 participants