Skip to content

test(http): lock AbortSignal behavior for http.request options.signal - #34567

Closed
armancharan wants to merge 2 commits into
oven-sh:mainfrom
armancharan:http-request-abort-timeout
Closed

armancharan wants to merge 2 commits into
oven-sh:mainfrom
armancharan:http-request-abort-timeout

Conversation

@armancharan

Copy link
Copy Markdown

Summary

  • Closes #31167.
  • The runtime fix already landed on main via the net/tls + llhttp client rewrite (#31587); #31170 correctly closed as obsolete.
  • What never landed was the regression suite. This PR adds focused tests so options.signal / AbortSignal.timeout cannot silently regress.
  • Includes the original blackhole repro (http://10.255.255.1/hang): on Bun 1.3.14 the test times out; on current main it settles with AbortError (ABORT_ERR) in ~150ms.

Why tests-only

Re-implementing the old fetch-client patch would be incorrect. Depth here is verifying the rewrite, documenting the gap, and locking Node-parity abort lifecycle with tests maintainers can keep.

Test plan

  • bun bd test test/js/node/http/client-signal-abort.test.ts — 7 pass
  • USE_SYSTEM_BUN=1 bun test … -t "unreachable host" — times out on 1.3.14 (bug still present in release)
  • CI green

Made with Cursor

Add regression coverage for oven-sh#31167: AbortSignal.timeout must emit
AbortError on http.request, including during connect to an unreachable
host. Behavior is already fixed on main via oven-sh#31587; this prevents regressing
it (still broken on 1.3.14).
@armancharan
armancharan marked this pull request as ready for review July 18, 2026 05:24

@claude claude Bot left a comment

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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The PR adds Node HTTP client regression tests covering AbortSignal behavior before, during, and after requests, including error metadata, event ordering, streaming responses, connection failures, and timeout settlement.

HTTP client AbortSignal behavior

Layer / File(s) Summary
Abort error contract
test/js/node/http/client-signal-abort.test.ts
Tests AbortError, ABORT_ERR, and signal-reason propagation for in-flight and pre-aborted requests.
Request lifecycle events
test/js/node/http/client-signal-abort.test.ts
Checks completed requests remain error-free, abort emits before close, and connection failures are not re-emitted.
Streaming and timeout settlement
test/js/node/http/client-signal-abort.test.ts
Tests aborting an open streaming response and verifies timeout abortion settles within one second for an unreachable host.

Possibly related PRs

  • oven-sh/bun#34360: Covers related Node HTTP client lifecycle behavior during mid-stream client abortion.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: regression tests for AbortSignal behavior in http.request.
Description check ✅ Passed It covers what the PR does and how it was verified, though it uses custom headings instead of the template's exact sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Comment thread test/js/node/http/client-signal-abort.test.ts Outdated
Comment thread test/js/node/http/client-signal-abort.test.ts Outdated
Drop PR-history preamble and clarify the blackhole timeout note.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
test/js/node/http/client-signal-abort.test.ts (5)

28-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the timeout cause by identity.

Checking only cause.name allows a newly created TimeoutError to pass. Assert that err.cause is exactly signal.reason.

As per coding guidelines, assertions should prove the strongest invariant. The supplied AbortSignal contract requires cause-object identity.

Proposed fix
-      expect((err as any).cause).toBeDefined();
-      expect((err as any).cause.name).toBe("TimeoutError");
+      expect((err as any).cause).toBe(signal.reason);
🤖 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 `@test/js/node/http/client-signal-abort.test.ts` around lines 28 - 31, Update
the timeout-cause assertions in the abort test to verify that err.cause is the
exact same object as signal.reason, rather than checking only that it exists and
has a "TimeoutError" name. Preserve the existing timeout scenario and use the
signal instance already exercised by the test.

Source: Coding guidelines


32-34: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Await server shutdown in every finally block.

server.close() is asynchronous; invoking it without awaiting completion can leave sockets or close events pending across tests.

As per coding guidelines, abort and timeout paths must complete resource release before the operation finishes.

Also applies to: 61-63, 93-95, 128-130, 190-192

🤖 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 `@test/js/node/http/client-signal-abort.test.ts` around lines 32 - 34, Update
every finally block in the test cases to await the asynchronous server.close()
call, including the blocks around the referenced abort and timeout paths. Ensure
each test completes server shutdown and associated resource cleanup before its
operation finishes.

Source: Coding guidelines


138-142: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use a dynamically closed local port instead of port 1.

Port 1 is not guaranteed to be unused. Allocate an ephemeral local port, close the listener, then connect to that captured port so the test deterministically exercises the connection-failure path.

As per coding guidelines, tests must be hermetic and avoid environment-dependent fixtures.

🤖 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 `@test/js/node/http/client-signal-abort.test.ts` around lines 138 - 142, Update
the request setup around req and controller to first allocate an ephemeral local
port with a temporary listener, capture its assigned port, close the listener,
and use that port instead of hardcoded port 1. Preserve the test’s
connection-failure and abort behavior while removing dependence on
environment-specific port availability.

Source: Coding guidelines


195-213: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Remove the environment-dependent blackhole address.

10.255.255.1 may fail immediately with ENETUNREACH or ECONNREFUSED depending on CI routing, so this test can fail before the 150 ms abort or validate the wrong path. Use a deterministic connect-stall fixture or a repository-supported network fixture.

As per coding guidelines, tests must be hermetic and avoid external hosts.

🤖 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 `@test/js/node/http/client-signal-abort.test.ts` around lines 195 - 213,
Replace the hard-coded blackhole URL in the AbortSignal.timeout test with a
deterministic repository-supported connect-stall fixture that keeps the
connection pending until the signal aborts. Preserve the existing request,
AbortError assertions, and prompt-settlement check while ensuring the test does
not depend on external network routing.

Source: Coding guidelines


174-187: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the streaming completion assertion observable.

reject(new Error(...)) is ineffective once the request-error promise has resolved, so a response that emits end after abort will not fail this test. Track end separately, await request closure, and assert that the response never completed.

As per coding guidelines, every assertion must be able to fail and tests must await observable conditions.

Proposed fix
       const controller = new AbortController();
+      let responseEnded = false;
       const { promise, resolve, reject } = Promise.withResolvers<Error>();
 
       const req = request(`http://127.0.0.1:${port}`, { signal: controller.signal }, res => {
         res.on("data", () => {
           controller.abort();
         });
         res.on("end", () => {
-          reject(new Error("response should not complete after abort"));
+          responseEnded = true;
         });
       });
       req.on("error", resolve);
       req.end();
 
       const err = await promise;
+      await once(req, "close");
       expect(err.name).toBe("AbortError");
       expect((err as any).code).toBe("ABORT_ERR");
+      expect(responseEnded).toBe(false);
🤖 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 `@test/js/node/http/client-signal-abort.test.ts` around lines 174 - 187, Update
the request test around the Promise.withResolvers flow so response end is
tracked independently from the request error promise. Await request closure
after abort, then assert that the response did not emit end; do not use reject
from the error promise as the completion assertion.

Source: Coding guidelines

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

Outside diff comments:
In `@test/js/node/http/client-signal-abort.test.ts`:
- Around line 28-31: Update the timeout-cause assertions in the abort test to
verify that err.cause is the exact same object as signal.reason, rather than
checking only that it exists and has a "TimeoutError" name. Preserve the
existing timeout scenario and use the signal instance already exercised by the
test.
- Around line 32-34: Update every finally block in the test cases to await the
asynchronous server.close() call, including the blocks around the referenced
abort and timeout paths. Ensure each test completes server shutdown and
associated resource cleanup before its operation finishes.
- Around line 138-142: Update the request setup around req and controller to
first allocate an ephemeral local port with a temporary listener, capture its
assigned port, close the listener, and use that port instead of hardcoded port
1. Preserve the test’s connection-failure and abort behavior while removing
dependence on environment-specific port availability.
- Around line 195-213: Replace the hard-coded blackhole URL in the
AbortSignal.timeout test with a deterministic repository-supported connect-stall
fixture that keeps the connection pending until the signal aborts. Preserve the
existing request, AbortError assertions, and prompt-settlement check while
ensuring the test does not depend on external network routing.
- Around line 174-187: Update the request test around the Promise.withResolvers
flow so response end is tracked independently from the request error promise.
Await request closure after abort, then assert that the response did not emit
end; do not use reject from the error promise as the completion assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4770c724-9b22-40e2-9337-6cf53235f729

📥 Commits

Reviewing files that changed from the base of the PR and between ddb2504 and 902dcb3.

📒 Files selected for processing (1)
  • test/js/node/http/client-signal-abort.test.ts

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Thanks for putting this together. #31167 was fixed on main by the http client rewrite in #31587, and that PR also brought in coverage for these cases:

  • the ClientRequest.signal test in test/js/node/http/node-http.test.ts: AbortSignal.timeout against a server that never responds must emit an AbortError on the request
  • test-http-client-abort-destroy.js: abort synchronously, on the next tick, and with an already aborted signal, each checking AbortError / ABORT_ERR and the close event
  • test-https-abortcontroller.js and test-http-agent-abort-controller.js: the same cases over https and with an explicit agent
  • test-http-client-close-event.js: the error is emitted before close

The one case here that main does not have, the request to 10.255.255.1, cannot run in CI: tests are not allowed to leave the machine, and unroutable addresses get refused rather than dropped inside the CI containers, so it would fail there.

Since the reported behavior is already covered on main, closing this one. If you would like to add hermetic versions of the streaming-body or abort-after-completion cases, a smaller follow-up would be welcome.

@robobun robobun closed this Aug 13, 2026
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.

AbortSignal.timeout() is not respected for http.request

2 participants