Skip to content

http: gate fetch TCP keepalive on the existing keepalive option#30640

Merged
cirospaciari merged 3 commits into
mainfrom
ciro/keepalive-configurable
May 13, 2026
Merged

http: gate fetch TCP keepalive on the existing keepalive option#30640
cirospaciari merged 3 commits into
mainfrom
ciro/keepalive-configurable

Conversation

@cirospaciari

@cirospaciari cirospaciari commented May 13, 2026

Copy link
Copy Markdown
Member

Includes #30637 (undici source link for the 60s default).

Mirror undici's options.keepAlive guard in buildConnector: when fetch(url, { keepalive: false }) is used (which already disables HTTP connection reuse), also skip the SO_KEEPALIVE setsockopt instead of applying it unconditionally.

node:http / node:https defaults (no changes needed)

ClientRequest already forwards agent.keepAlive to fetch's keepalive in _http_client.ts, and both global agents are constructed with { keepAlive: true, scheduling: "lifo", timeout: 5000 } — matching Node 19+:

agent.keepAlive TCP SO_KEEPALIVE
http.globalAgent / https.globalAgent true on (60s, undici default)
new Agent() / agent: false false off

How did you verify your code works?

test/js/web/fetch/fetch-tcp-keepalive.test.ts (Linux-only, reads /proc/self/net/tcp) extended with cases for keepalive: false, node:http agent: false, and node:http globalAgent.

cirospaciari and others added 3 commits May 13, 2026 14:05
The 60s keepalive added in b9c757b mirrors undici's default. Point the comment at the exact undici call site (lib/core/connect.js, nodejs/undici#1904) so the provenance of the value is traceable.
Mirror undici's `options.keepAlive` guard in `buildConnector`: when
`fetch(url, { keepalive: false })` is used (which already disables HTTP
connection reuse), also skip the SO_KEEPALIVE setsockopt instead of
applying it unconditionally.

`node:http`/`node:https` already forward `agent.keepAlive` to fetch's
`keepalive` option in `_http_client.ts`, and both `http.globalAgent` and
`https.globalAgent` are constructed with `{ keepAlive: true }` (matching
Node 19+ defaults), so requests through the global agents get TCP
keepalive while `agent: false` or a fresh `new Agent()` (whose
`keepAlive` defaults to `false`) skip it — same as Node.
@robobun

robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator
Updated 4:53 PM PT - May 13th, 2026

@cirospaciari, your commit b1cc618 has 3 failures in Build #54099 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30640

That installs a local version of the PR into your bun-30640 executable, so you can run:

bun-30640 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Request getters return fixed values, ignoring init: referrer, referrerPolicy, integrity, keepalive #30124 - Reports that Request.keepalive getter returns undefined regardless of the init value passed; this PR properly wires the keepalive option through fetch internals
  2. http.request and fetch on keep-alive opens always new socket, even it shouldn't #12053 - Reports that http.request with Agent({ keepAlive: true }) opens a new socket every time; this PR correctly gates TCP keepalive on the agent's keepAlive setting to match Node.js behavior

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #30124
Fixes #12053

🤖 Generated with Claude Code

@cirospaciari cirospaciari marked this pull request as ready for review May 13, 2026 22:24
@cirospaciari

Copy link
Copy Markdown
Member Author

@robobun adopt

@robobun

robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator

✅ Adopted & verified — all 4 cases in fetch-tcp-keepalive.test.ts pass locally (Linux x64 ASAN debug), and the two new keepalive: false / agent: false cases correctly fail with src/http/http.zig reverted to main.

CI #54099: the diff's own test file passes on debian x64-asan. The red lanes are pre-existing flakes that also hit other unrelated PRs this week:

test lane also failing on
fetch-http2-client.test.ts (ThreadLock / AtomStringImpl assert) debian x64-asan #30636, #30403, #30617, #30606, #29473
test-http-should-emit-close-when-connection-is-aborted.ts timeout Windows ×3 ~20 of the last 50 builds
node-http-backpressure-max.test.ts 60s timeout darwin x64 #30636, build 53924

All three pass locally with this change applied; none go through the onOpensetKeepAlive codepath in a way the added if (!client.flags.disable_keepalive) guard could affect (default keepalive: true ⇒ guard is a no-op). Should be safe to merge through.

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

LGTM — straightforward gate on an existing flag, matching undici's options.keepAlive guard, with thorough Linux-only test coverage.

Extended reasoning...

Overview

This PR wraps the recently-added socket.setKeepAlive(true, 60) call in onOpen (src/http/http.zig) with a guard on the pre-existing client.flags.disable_keepalive flag. The net functional change is a single if condition. The rest is a refactor of fetch-tcp-keepalive.test.ts to extract a probeClientSocket helper and add three new cases covering fetch({ keepalive: false }), node:http with agent: false, and node:http with globalAgent.

Security risks

None. The change only skips a setsockopt(SO_KEEPALIVE) call when the caller has explicitly opted out of keepalive. Default behavior (the path used by bun install, plain fetch(), and http.globalAgent) is unchanged. No new inputs are parsed and no new syscalls are introduced.

Level of scrutiny

Low. disable_keepalive is an established field on Flags (http.zig:644) already used to gate HTTP connection reuse and the Connection header throughout this file, and I confirmed _http_client.ts:298-302 forwards agent.keepAlive → fetch keepalive exactly as the comment and PR description claim. The semantics — conflating HTTP-level keepalive with TCP SO_KEEPALIVE — are an intentional mirror of undici's buildConnector (linked in the comment), not an accidental conflation.

Other factors

Tests are well-constructed: each case spins up a fresh server on port 0 so the connection pool can't reuse a socket from a prior test, and assertions check both the positive (02) and negative (00) timer states. No outstanding reviewer comments and no bugs flagged by the bug-hunting system.

@cirospaciari cirospaciari changed the base branch from claude/keepalive-undici-ref to main May 13, 2026 22:41
@cirospaciari cirospaciari enabled auto-merge (squash) May 13, 2026 23:52
@cirospaciari cirospaciari disabled auto-merge May 13, 2026 23:52
@cirospaciari cirospaciari merged commit b8ecc78 into main May 13, 2026
74 of 79 checks passed
@cirospaciari cirospaciari deleted the ciro/keepalive-configurable branch May 13, 2026 23:52
dylan-conway added a commit that referenced this pull request May 14, 2026
### What does this PR do?

Ports the TCP keepalive call from `src/http/http.zig` `onOpen` (added in
#30627, gated in #30640) into the Rust `on_open` in `src/http/lib.rs`.
The Rust HTTP client landed in #30412 from a branch cut before #30627
merged, so the `set_keepalive` call was never carried over.

This is the cause of `test/js/web/fetch/fetch-tcp-keepalive.test.ts`
failing on every Linux test job since #30412 merged. The test reads
`/proc/self/net/tcp` and expects `timer_active=02` (keepalive armed) but
gets `00` because the Rust client never calls `set_keepalive`. Build
#54331 (and every Linux job since): 2 pass / 2 fail; build #54099
(#30640's PR, Zig build): 4 pass / 0 fail; build #54202 (#30412's PR):
test file not in tree, so it never ran.

The change is the same shape as the Zig reference: after
`self.set_timeout(socket)`, guarded by `!self.flags.disable_keepalive`
so `fetch(url, { keepalive: false })` and `node:http` non-keepalive
agents skip it (matching undici's `buildConnector`). `set_keep_alive`
already exists on `NewSocketHandler` (`src/uws_sys/socket.rs:466`) and
the `disable_keepalive` flag is already wired through.

### How did you verify your code works?

I have not built or run this — opening it directly per request so CI
validates. The covering test is
`test/js/web/fetch/fetch-tcp-keepalive.test.ts` (Linux-only), which is
the test currently failing on every PR.
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.

3 participants