Skip to content

WebSocket client: support ws+unix:// and wss+unix://#29203

Merged
Jarred-Sumner merged 5 commits intomainfrom
farm/aef1f233/websocket-unix-client
Apr 12, 2026
Merged

WebSocket client: support ws+unix:// and wss+unix://#29203
Jarred-Sumner merged 5 commits intomainfrom
farm/aef1f233/websocket-unix-client

Conversation

@robobun
Copy link
Copy Markdown
Collaborator

@robobun robobun commented Apr 12, 2026

What does this PR do?

Adds Unix domain socket support to the WebSocket client via the ws+unix:// and wss+unix:// URL schemes.

// Plain
const ws = new WebSocket("ws+unix:///tmp/app.sock");

// With a request path (split on first ':', same as the npm `ws` package)
const ws = new WebSocket("ws+unix:///tmp/app.sock:/api/stream?x=1");

// TLS over a unix socket
const ws = new WebSocket("wss+unix:///tmp/app.sock", {
  tls: { rejectUnauthorized: false },
});
  • Host header defaults to localhost, matching Node's http.request({ socketPath }) and ws.
  • Proxies are ignored for unix URLs (the socket is local).
  • wss+unix:// selects the TLS socket context and runs the normal handshake over the domain socket.

How did you verify your code works?

New test file test/js/web/websocket/websocket-unix.test.ts covers:

  • echo over Bun.serve({ unix })
  • :path + query string parsing and Host header value
  • binary frames
  • connect failure when the socket file does not exist
  • SyntaxError on missing socket path
  • wss+unix:// against Bun.serve({ unix, tls })
  • round-trip from a spawned subprocess
 7 pass
 0 fail

Existing websocket-client.test.ts and websocket-custom-headers.test.ts continue to pass.

Implementation

  • WebSocket.cpp: recognise ws+unix: / wss+unix:, split pathname on the first : into socket path + request path, default host to localhost, skip proxy, pass the socket path through to Zig.
  • WebSocketUpgradeClient.zig: new unix_socket_path arg; when set, dial via Socket.connectUnixAnon on the existing per-SSL us_socket_context instead of the TCP connectPtr path. All downstream state (upgrade, adopt, deflate, custom SSL ctx) is unchanged.
  • headers.h: add the parameter to both Bun__WebSocketHTTP{,S}Client__connect externs.

Fixes #4423

The WebSocket client can now connect over Unix domain sockets using
ws+unix:///path/to/sock[:/request/path] (plaintext) or wss+unix://
(TLS). The URL format matches the npm 'ws' package: the pathname is
split on the first ':' into a socket path and an HTTP request path,
and the Host header defaults to 'localhost'.

The upgrade client reuses the existing uSockets unix-connect path
(us_socket_context_connect_unix) on the same per-SSL socket context,
so no new context is required. wss+unix:// selects the TLS socket
context and runs the normal TLS handshake over the domain socket.
@robobun
Copy link
Copy Markdown
Collaborator Author

robobun commented Apr 12, 2026

Updated 12:01 AM PT - Apr 12th, 2026

@robobun, your commit 95458e5 has 1 failures in Build #45254 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29203

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

bun-29203 --bun

@github-actions
Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Support ws+unix: in WebSocket client #4423 - Directly requests ws+unix: support in the WebSocket client, which is exactly what this PR implements

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

Fixes #4423

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Add WebSocket support for Unix domain sockets #25485 - Also adds WebSocket client support for Unix domain sockets (ws+unix:// and wss+unix://) by modifying the same core files (WebSocket.cpp and WebSocketUpgradeClient.zig)

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d3c55b42-1569-45c4-b9db-97b3b077542a

📥 Commits

Reviewing files that changed from the base of the PR and between 012e687 and 95458e5.

📒 Files selected for processing (1)
  • src/bun.js/bindings/webcore/WebSocket.cpp

Walkthrough

Adds ws+unix/wss+unix WebSocket support: extends C-ABI connect signatures to accept a unix socket path, updates URL parsing and connection flow to split socket vs HTTP resource, disables proxying for unix sockets, implements Unix-socket connect in Zig client, and adds comprehensive unix-socket tests.

Changes

Cohort / File(s) Summary
C-ABI Bindings
src/bun.js/bindings/headers.h
Extended Bun__WebSocketHTTPClient__connect and Bun__WebSocketHTTPSClient__connect signatures with a trailing const BunString* unixSocketPath parameter.
WebCore WebSocket glue
src/bun.js/bindings/webcore/WebSocket.cpp
Recognize ws+unix/wss+unix, mark is_unix and adjust is_secure, parse m_url.path() into unixSocketPath and HTTP resource (split on first : with / default), enforce pathname/socket non-empty, default empty Host to localhost, disable proxying for unix sockets, and pass unixSocketPath (or nullptr) into Zig connect calls.
Zig WebSocket client
src/http/websocket_client/WebSocketUpgradeClient.zig
Added unix_socket_path: ?*const bun.String to connect(...); decode UTF‑8 path, short‑circuit to Socket.connectUnixAnon(...), set socket/timeouts/state on success, handle failure cleanup, and adjust hostname/SNI behavior for TLS over Unix.
Tests
test/js/web/websocket/websocket-unix.test.ts
New test suite (skipped on Windows) exercising ws+unix/wss+unix echo flows, path/query mapping, default Host, binary frames, missing-socket failures, SyntaxError validation, TLS over unix, and subprocess end‑to‑end behavior.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding Unix domain socket support to WebSocket client via two URL schemes.
Description check ✅ Passed The description is comprehensive, including what the PR does, usage examples, implementation details, and verification through tests with passing results.
Linked Issues check ✅ Passed The PR fully implements the requirement from issue #4423 to support ws+unix:// URLs in the WebSocket client, with proper socket path handling and both plain and TLS variants.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing ws+unix:// and wss+unix:// support: C++ header signatures, Zig socket connection logic, WebSocket URL parsing, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/http/websocket_client/WebSocketUpgradeClient.zig`:
- Around line 331-337: The code sets TLS SNI/verification from host_slice
(URL/default) which ignores an explicit Host header override and breaks
wss+unix:// verification; change the logic that assigns client.hostname (in
WebSocketUpgradeClient.zig around host_slice) to use the effective host: prefer
the request's Host header (the override) if present, otherwise fall back to the
URL hostname, then only set client.hostname when that effective host is
non-empty and not an IP (same isIPAddress check); ensure you reference and
derive the effective host (hostname orelse url.hostname) before the existing
client.hostname assignment so TLS verification uses the override when supplied.

In `@test/js/web/websocket/websocket-unix.test.ts`:
- Around line 207-210: Remove the fragile stderr assertion in the test: delete
the line using expect(stderr).not.toContain("error") and rely on the existing
checks of stdout.trim() === "pong:from-child" and exitCode === 0 (the variables
proc, stdout, stderr, exitCode are in the surrounding code); only add stderr
assertions if you need to assert a specific expected stderr message.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 235d7cf5-d8bf-41e6-ab5f-ca9735d3feeb

📥 Commits

Reviewing files that changed from the base of the PR and between 4d78bd1 and 013cc23.

📒 Files selected for processing (4)
  • src/bun.js/bindings/headers.h
  • src/bun.js/bindings/webcore/WebSocket.cpp
  • src/http/websocket_client/WebSocketUpgradeClient.zig
  • test/js/web/websocket/websocket-unix.test.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
test/js/web/websocket/websocket-unix.test.ts (1)

207-210: ⚠️ Potential issue | 🟡 Minor

Remove the fragile generic stderr check on Line 209.

expect(stderr).not.toContain("error") is noisy/flaky in CI (e.g., sanitizer/runtime diagnostics). stdout.trim() and exitCode already validate the round-trip outcome.

Proposed fix
-    const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
+    const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
     expect(stdout.trim()).toBe("pong:from-child");
-    expect(stderr).not.toContain("error");
     expect(exitCode).toBe(0);

As per coding guidelines, only assert stderr when checking for a specific expected error message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/js/web/websocket/websocket-unix.test.ts` around lines 207 - 210, Remove
the fragile generic stderr assertion that checks for the substring "error" and
rely on the existing definitive assertions (stdout.trim() equals
"pong:from-child" and exitCode equals 0); specifically remove the line
referencing stderr.not.toContain("error") that uses the stderr variable, and
only add a stderr assertion if you have a precise expected stderr string to
validate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/js/web/websocket/websocket-unix.test.ts`:
- Around line 207-210: Remove the fragile generic stderr assertion that checks
for the substring "error" and rely on the existing definitive assertions
(stdout.trim() equals "pong:from-child" and exitCode equals 0); specifically
remove the line referencing stderr.not.toContain("error") that uses the stderr
variable, and only add a stderr assertion if you have a precise expected stderr
string to validate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a6d7b4fc-2c35-4e48-8eed-7865f81956d4

📥 Commits

Reviewing files that changed from the base of the PR and between 013cc23 and 37d3462.

📒 Files selected for processing (1)
  • test/js/web/websocket/websocket-unix.test.ts

The comment claimed a user-supplied Host header drives SNI, but the
implementation (like the TCP path) only uses the URL host. Correct the
comment to describe what actually happens and point at the workarounds.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/http/websocket_client/WebSocketUpgradeClient.zig`:
- Around line 338-340: Replace the silent OOM fallback when duplicating
host_slice so allocation failures crash instead of setting an empty hostname:
change the expression assigning client.hostname (the call to
bun.default_allocator.dupeZ(u8, host_slice.slice()) catch "") to handle errors
via bun.handleOom() for error.OutOfMemory and propagate any other errors instead
of swallowing them; ensure the surrounding function (in
WebSocketUpgradeClient.zig) has the appropriate error union to allow propagating
non-OOM errors and that strings.isIPAddress(host_slice.slice()) logic remains
unchanged.
- Around line 318-327: The unix-path can set client.tcp too late causing
handleConnectError() to detach the wrong sentinel; change the lifetime handling
in the Socket.connectUnixAnon branch to mirror the Socket.connectPtr() pattern
used elsewhere: assign client.tcp (or otherwise take ownership of the returned
socket) immediately after the connect succeeds, or if you must keep the current
ordering ensure you call client.tcp.detach() (or the equivalent detach method)
before calling client.deref() when client.state == .failed so the real unix
socket is detached rather than an old sentinel; update the
Socket.connectUnixAnon handling to match handleConnectError()/connectPtr
lifetime semantics to avoid tearing down a client whose tcp was never detached.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4d492598-713e-40e7-a6ff-119c9f77ccf6

📥 Commits

Reviewing files that changed from the base of the PR and between 21821bb and 012e687.

📒 Files selected for processing (1)
  • src/http/websocket_client/WebSocketUpgradeClient.zig

ws+unix:///sock:api/v1 previously produced 'GET api/v1 HTTP/1.1',
which is not valid origin-form per RFC 7230 section 5.3.1. Prepend
a '/' when the user-supplied request path after the colon does not
already start with one.
Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

This is a well-iterated feature PR — the two issues I raised earlier (misleading SNI comment, missing leading-slash normalization on the request path) were both fixed. No bugs remain open. Because the change touches core WebSocket networking code across C++, Zig, and the ABI boundary, I'll leave final sign-off to a human maintainer.

Extended reasoning...

Overview

Adds and URL scheme support to the WebSocket client. Changes span four files: (ABI signature update for both connect functions), (URL parsing, socket-path/request-path splitting, proxy bypass, Host defaulting), (new branch mirroring the TCP path), and a new test file covering 7 scenarios.

Security risks

No security concerns introduced. Unix domain socket connections are inherently local; the PR correctly disables proxy routing for them. TLS verification on uses the URL host for SNI (defaulting to 'localhost'), mirroring the TCP path — intentional and documented in the comment.

Level of scrutiny

Medium-high. The C++ ↔ Zig ABI is extended (both signatures gain a trailing parameter), the URL-parsing logic handles a new scheme family, and the Zig connect path is new code. All of these areas have been reviewed iteratively and the two bugs I found were fixed.

Other factors

Two bugs were found and fixed during review: a misleading comment about Host-header-driven SNI (commit 012e687) and missing leading-slash normalization on the colon-separated request path (commit 95458e5). The fragile stderr assertion was removed in commit 21821bb. All inline thread discussions are resolved. Test coverage is comprehensive (echo, path/query parsing, binary, connect failure, SyntaxError, TLS, subprocess).

@Jarred-Sumner Jarred-Sumner merged commit b18f268 into main Apr 12, 2026
61 of 63 checks passed
@Jarred-Sumner Jarred-Sumner deleted the farm/aef1f233/websocket-unix-client branch April 12, 2026 04:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support ws+unix: in WebSocket client

2 participants