Skip to content

fix(node:http): hand off upgrade socket to userland for bidirectional communication#28158

Closed
robobun wants to merge 2 commits into
mainfrom
claude/fix-http-upgrade-socket-28157
Closed

fix(node:http): hand off upgrade socket to userland for bidirectional communication#28158
robobun wants to merge 2 commits into
mainfrom
claude/fix-http-upgrade-socket-28157

Conversation

@robobun

@robobun robobun commented Mar 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fix node:http server upgrade socket not being handed off to userland after the "upgrade" event, causing socket.write() to silently drop data and post-upgrade client data to get 400 Bad Request
  • Enable streaming on upgrade sockets (matching CONNECT handler), mark connection as raw mode in uWebSockets to stop HTTP parsing, and return early with a socket-close promise

Root Cause

Two issues in src/js/node/_http_server.ts:

  1. Write side: socket[kEnableStreaming](false) was called for all non-CONNECT requests (line 555), but the upgrade handler never re-enabled it. This left handle.ondrain as undefined, causing _write() to silently skip handle.write().

  2. Read side: The upgrade handler didn't return early — it fell through to socket.cork() and the HTTP response promise. More critically, uWebSockets' HTTP parser continued owning the connection, parsing post-upgrade data as new HTTP requests (→ 400 Bad Request).

Fix

Mirror the existing CONNECT handler pattern:

  • Call socket[kEnableStreaming](true) before emitting the upgrade event
  • Call socketHandle.markAsRawMode() to set isConnectRequest on the uWebSockets HttpResponseData, switching the HTTP parser to raw pass-through mode
  • Return a promise resolved on socket close (instead of falling through to cork/HTTP response lifecycle)

The markAsRawMode() method is new — exposed through the full stack: HttpResponse.hlibuwsockets.cppResponse.zigJSNodeHTTPServerSocket prototype.

Test plan

  • New regression test test/regression/issue/28157.test.ts verifies bidirectional communication after upgrade
  • Test fails with system bun (v1.3.10), passes with debug build
  • test/js/web/fetch/fetch.upgrade.test.ts passes
  • test/js/node/http/node-http-with-ws.test.ts passes (WebSocket over node:http)
  • test/js/node/http/node-http-connect.test.ts CONNECT raw socket test passes

Closes #28157

🤖 Generated with Claude Code

… communication

After the "upgrade" event was emitted on a node:http server, the TCP
connection was not properly handed off to userland. Two issues:

1. socket.write() silently dropped data because kEnableStreaming(true)
   was never called for upgrade connections (only for CONNECT).

2. The HTTP parser (uWebSockets) continued parsing incoming data as
   HTTP requests, producing 400 Bad Request for post-upgrade data.

Fix by mirroring the CONNECT handler: enable streaming, mark the
connection as raw mode (setting isConnectRequest on HttpResponseData
to stop HTTP parsing), and return a promise resolved on socket close
instead of falling through to the HTTP response lifecycle.

Closes #28157

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@robobun

robobun commented Mar 16, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:09 AM PT - Mar 16th, 2026

@autofix-ci[bot], your commit 0f2be2c has 3 failures in Build #39738 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 28158

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

bun-28158 --bun

@coderabbitai

coderabbitai Bot commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds markAsRawMode() capability across the runtime stack to prevent HTTP parsing on upgraded connections. Implements raw mode flag at C++, C FFI, and JavaScript layers, then uses it in the HTTP server upgrade handler to properly hand off connections to userland without further HTTP parsing.

Changes

Cohort / File(s) Summary
Core HTTP Response Implementation
packages/bun-uws/src/HttpResponse.h
Added public markAsRawMode() method to HttpResponse class that sets the isConnectRequest flag, disabling HTTP parsing for raw bidirectional communication.
JavaScript Bindings
src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
Added jsFunctionNodeHTTPServerSocketMarkAsRawMode host function with prototype wiring to expose the raw mode capability to JavaScript via the NodeHTTPServerSocket object.
Low-level FFI Bindings
src/deps/libuwsockets.cpp, src/deps/uws/Response.zig
Added C extern function uws_res_mark_as_raw_mode() and corresponding Zig bindings to bridge C++ implementation to higher-level layers, supporting both SSL and non-SSL paths.
HTTP Server Upgrade Handler
src/js/node/_http_server.ts
Refactored upgrade event flow to call markAsRawMode() on the socket, enable streaming, and return a Promise that resolves on socket close, deferring internal socket assignment to after upgrade event processing.
Regression Test
test/regression/issue/28157.test.ts
Added end-to-end test spawning a Node HTTP server with upgrade handler and validating bidirectional communication via echoed data without HTTP parsing errors.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing the upgrade socket handoff in node:http for bidirectional communication by enabling streaming and marking the connection as raw mode.
Description check ✅ Passed The PR description provides comprehensive details covering what the PR does, the root cause analysis, the fix approach, and thorough test plan verification, exceeding the basic template requirements.
Linked Issues check ✅ Passed The PR directly addresses all coding requirements from issue #28157: enabling streaming on upgrade sockets, marking connections as raw mode to stop HTTP parsing, returning a socket-close promise, and adding regression test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the upgrade socket handoff issue: exposing markAsRawMode() through the stack, implementing the fix in _http_server.ts, and adding regression tests.

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

📝 Coding Plan
  • Generate coding plan for human review comments

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

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

Actionable comments posted: 3

🤖 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/js/node/_http_server.ts`:
- Around line 617-618: The upgrade branch currently emits server.emit("upgrade",
http_req, socket, kEmptyBuffer) which drops any buffered post-header bytes;
change that emit to forward the actual buffered head (use connectHead ??
kEmptyBuffer) so buffered bytes reached userland (match the CONNECT path
behavior) and ensure HttpRequest.headData/headLength are not cleared after
onNodeHTTPRequest/upgrade handling so callers can access the captured bytes;
update the code around server.emit("upgrade", http_req, socket, ...) and any
post-handler clearing logic to preserve headData/headLength.

In `@test/regression/issue/28157.test.ts`:
- Around line 61-64: Remove the in-child 5s watchdog (the setTimeout block that
calls console.error("TIMEOUT") and process.exit(1)) and instead enforce time
limits from the parent test harness: delete the
setTimeout/...console.error/...process.exit(1) block in issue/28157.test.ts and
implement the timeout in the parent test (e.g., use the test framework's timeout
API or the parent spawn/childProcess timeout options and give a larger overall
timeout) so the parent kills the child on timeout rather than the child
self-aborting.
- Around line 33-40: The test currently sends the protocol payload only after
parsing the 101 response, so it never exercises the buffered-head code path;
update the net.connect/client.write call in the test to append the first
protocol payload (e.g. "hello from client") directly to the initial HTTP request
bytes so the server receives headers and protocol data in the same read, and
make the same change for the other occurrence noted (lines 49-52). Also ensure
the server-side behavior preserves HttpRequest.headData and
HttpRequest.headLength after requestHandler returns (do not clear them) so
assertions about buffered head data remain valid; reference the
net.connect/client.write call in the test and the
HttpRequest.headData/headLength and requestHandler symbols when making these
changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 208a76b8-1591-4b40-a3a0-2476da2d4193

📥 Commits

Reviewing files that changed from the base of the PR and between d50ab98 and 0f2be2c.

📒 Files selected for processing (6)
  • packages/bun-uws/src/HttpResponse.h
  • src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
  • src/deps/libuwsockets.cpp
  • src/deps/uws/Response.zig
  • src/js/node/_http_server.ts
  • test/regression/issue/28157.test.ts

Comment on lines 617 to +618
server.emit("upgrade", http_req, socket, kEmptyBuffer);
if (!socket._httpMessage) {
if (canUseInternalAssignSocket) {
// ~10% performance improvement in JavaScriptCore due to avoiding .once("close", ...) and removing a listener
assignSocketInternal(http_res, socket);
} else {
http_res.assignSocket(socket);
}
}
return upgradePromise;

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.

⚠️ Potential issue | 🟠 Major

Don't drop buffered post-header bytes in the upgrade path.

connectHead is already threaded into onNodeHTTPRequest, but this branch always emits kEmptyBuffer. If the parser has already captured bytes beyond \r\n\r\n, they never reach userland here. Forward connectHead ?? kEmptyBuffer, like the CONNECT path does.

Suggested change
-          server.emit("upgrade", http_req, socket, kEmptyBuffer);
+          const head = connectHead ?? kEmptyBuffer;
+          server.emit("upgrade", http_req, socket, head);
           return upgradePromise;

Based on learnings, headData and headLength on HttpRequest are intentionally left populated for all request types and should not be cleared after the requestHandler call.

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

In `@src/js/node/_http_server.ts` around lines 617 - 618, The upgrade branch
currently emits server.emit("upgrade", http_req, socket, kEmptyBuffer) which
drops any buffered post-header bytes; change that emit to forward the actual
buffered head (use connectHead ?? kEmptyBuffer) so buffered bytes reached
userland (match the CONNECT path behavior) and ensure
HttpRequest.headData/headLength are not cleared after onNodeHTTPRequest/upgrade
handling so callers can access the captured bytes; update the code around
server.emit("upgrade", http_req, socket, ...) and any post-handler clearing
logic to preserve headData/headLength.

Comment on lines +33 to +40
const client = net.connect(port, "127.0.0.1", () => {
client.write(
"GET / HTTP/1.1\\r\\n" +
"Host: 127.0.0.1\\r\\n" +
"Upgrade: custom-protocol\\r\\n" +
"Connection: Upgrade\\r\\n" +
"\\r\\n"
);

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.

🧹 Nitpick | 🔵 Trivial

Cover the buffered-head path in this regression.

The client only sends hello from client after it has parsed the 101 response, so this test never exercises bytes that arrive in the same read as the upgrade request. Sending the first protocol payload together with the HTTP headers would catch a dropped head handoff too.

Based on learnings, headData and headLength on HttpRequest are intentionally left populated for all request types and should not be cleared after the requestHandler call.

Also applies to: 49-52

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

In `@test/regression/issue/28157.test.ts` around lines 33 - 40, The test currently
sends the protocol payload only after parsing the 101 response, so it never
exercises the buffered-head code path; update the net.connect/client.write call
in the test to append the first protocol payload (e.g. "hello from client")
directly to the initial HTTP request bytes so the server receives headers and
protocol data in the same read, and make the same change for the other
occurrence noted (lines 49-52). Also ensure the server-side behavior preserves
HttpRequest.headData and HttpRequest.headLength after requestHandler returns (do
not clear them) so assertions about buffered head data remain valid; reference
the net.connect/client.write call in the test and the
HttpRequest.headData/headLength and requestHandler symbols when making these
changes.

Comment on lines +61 to +64
setTimeout(() => {
console.error("TIMEOUT");
process.exit(1);
}, 5000);

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.

⚠️ Potential issue | 🟡 Minor

Move the watchdog out of the child process.

This fixed 5s setTimeout makes the regression time-based and can race the parent test timeout on slow CI. Prefer enforcing the timeout from the parent side and giving the overall test more slack than the child watchdog.

Based on learnings, spawned parent/child tests need explicit timeouts because the default 5000ms is often insufficient; as per coding guidelines, "Do not use setTimeout in tests."

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

In `@test/regression/issue/28157.test.ts` around lines 61 - 64, Remove the
in-child 5s watchdog (the setTimeout block that calls console.error("TIMEOUT")
and process.exit(1)) and instead enforce time limits from the parent test
harness: delete the setTimeout/...console.error/...process.exit(1) block in
issue/28157.test.ts and implement the timeout in the parent test (e.g., use the
test framework's timeout API or the parent spawn/childProcess timeout options
and give a larger overall timeout) so the parent kills the child on timeout
rather than the child self-aborting.

Comment on lines 608 to +618
} else if (is_upgrade) {
// Hand off the connection to userland, mirroring CONNECT handler.
// Enable streaming so socket.write() and socket.on("data") work.
socket[kEnableStreaming](true);
// Tell uWebSockets to stop parsing HTTP on this connection,
// switching to raw pass-through mode for bidirectional data.
socketHandle.markAsRawMode();
const { promise: upgradePromise, resolve: upgradeResolve } = $newPromiseCapability(Promise);
socket.once("close", upgradeResolve);
server.emit("upgrade", http_req, socket, kEmptyBuffer);
if (!socket._httpMessage) {
if (canUseInternalAssignSocket) {
// ~10% performance improvement in JavaScriptCore due to avoiding .once("close", ...) and removing a listener
assignSocketInternal(http_res, socket);
} else {
http_res.assignSocket(socket);
}
}
return upgradePromise;

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.

🔴 The upgrade handler (line 608) unconditionally calls socket[kEnableStreaming](true), socketHandle.markAsRawMode(), and returns a promise without first checking server.listenerCount("upgrade") > 0. If a client sends a request with an Upgrade header but the server has no "upgrade" listeners, the socket is permanently put in raw mode (HTTP parser disabled) with no handler to read or write it, leaving it hanging until timeout. The fix should mirror the CONNECT handler at line 538: check server.listenerCount("upgrade") > 0 before entering raw mode, and destroy the socket if no listeners exist.

Extended reasoning...

What the bug is

The new upgrade handler at lines 608-618 unconditionally enters raw mode and returns a promise, without checking whether any "upgrade" event listeners are registered on the server. This is in contrast to the CONNECT handler at line 538, which correctly guards with server.listenerCount("connect") > 0 before doing the same operations.

How it manifests

When a client sends an HTTP request with an Upgrade header (e.g., Upgrade: custom-protocol or Upgrade: websocket) to a server that has NO "upgrade" event listener, the following sequence occurs:

  1. is_upgrade is truthy at line 588 (it checks http_req.headers.upgrade, which is the header value)
  2. socket[kEnableStreaming](true) is called at line 611
  3. socketHandle.markAsRawMode() is called at line 614, which sets isConnectRequest = true on the uWebSockets HttpResponseData, permanently disabling the HTTP parser for this connection
  4. A promise is created that only resolves when the socket closes (line 615-616)
  5. server.emit("upgrade", ...) fires at line 617, but since there are no listeners, this is a no-op
  6. The function returns upgradePromise at line 618, which never resolves because nothing closes the socket

Why existing code doesn't prevent it

The is_upgrade check at line 588 only tests whether the Upgrade header is present in the request — it does NOT check whether the server has any "upgrade" event listeners. The CONNECT handler correctly separates these concerns: it checks the method (method === "CONNECT") and then separately checks server.listenerCount("connect") > 0. The upgrade handler skips the listener count check entirely.

Impact

The socket is left in an irrecoverable state: the HTTP parser is permanently disabled via markAsRawMode(), so even if the connection could somehow continue, it cannot parse any further HTTP. The socket will hang until the idle timeout fires (or indefinitely if timeout is set to 0, which is the default for this server). This is also a potential resource exhaustion vector — an attacker could send many requests with Upgrade headers to a server without upgrade listeners, tying up connections.

Step-by-step proof

  1. Create an HTTP server with http.createServer() — no "upgrade" listener attached
  2. A client sends: GET / HTTP/1.1\r\nHost: localhost\r\nUpgrade: custom\r\nConnection: Upgrade\r\n\r\n
  3. is_upgrade evaluates to "custom" (truthy) at line 588
  4. Code enters the else if (is_upgrade) branch at line 608
  5. markAsRawMode() permanently disables the HTTP parser
  6. server.emit("upgrade", ...) is a no-op — nobody is listening
  7. The returned promise never resolves because nothing writes to or closes the socket
  8. The socket hangs in raw mode, consuming a connection slot

How to fix

Mirror the CONNECT handler pattern. Before entering raw mode, check server.listenerCount("upgrade") > 0. If no listeners exist, destroy the socket (matching Node.js behavior) or close it via socketHandle.close().

Comment on lines 617 to +618
server.emit("upgrade", http_req, socket, kEmptyBuffer);
if (!socket._httpMessage) {
if (canUseInternalAssignSocket) {
// ~10% performance improvement in JavaScriptCore due to avoiding .once("close", ...) and removing a listener
assignSocketInternal(http_res, socket);
} else {
http_res.assignSocket(socket);
}
}
return upgradePromise;

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.

🔴 The upgrade handler at line 617 always passes kEmptyBuffer as the head argument to server.emit("upgrade", ...), discarding any buffered post-header bytes (connectHead) that arrived in the same TCP segment as the upgrade request. The CONNECT handler at line 546 correctly forwards connectHead ?? kEmptyBuffer. The fix is to change line 617 to: server.emit("upgrade", http_req, socket, connectHead ?? kEmptyBuffer).

Extended reasoning...

What the bug is

The onNodeHTTPRequest callback receives connectHead as its last parameter (line 524), which contains any bytes the HTTP parser found beyond the \r\n\r\n header terminator in the same TCP read. Per the Node.js "upgrade" event contract, the third argument (head) should be a Buffer containing the first packet of the upgraded protocol stream.

The inconsistency

The CONNECT handler at line 546 correctly forwards this buffer:

const head = connectHead ? connectHead : kEmptyBuffer;
server.emit("connect", http_req, socket, head);

But the upgrade handler at line 617 ignores connectHead entirely:

server.emit("upgrade", http_req, socket, kEmptyBuffer);

Why the data is permanently lost

The C++ layer (NodeHTTP.cpp lines 477-484) populates request->head for ALL request types (not just CONNECT) with any data found after the headers in the same read buffer. The comment in the C++ code explicitly says: "Pass pipelined data (head buffer) for Node.js compat (connect/upgrade events)". Once markAsRawMode() is called (line 614), the HTTP parser stops owning the connection. The pipelined bytes have already been consumed from the TCP buffer by the parser, so they will never be re-delivered through the raw socket data path.

Step-by-step proof

  1. A client sends a single TCP segment containing: GET / HTTP/1.1\r\nUpgrade: custom\r\nConnection: Upgrade\r\n\r\nHELLO
  2. The HTTP parser finds headers up to \r\n\r\n and stores HELLO in request->head
  3. onNodeHTTPRequest is called with connectHead = Buffer("HELLO")
  4. The code enters the else if (is_upgrade) branch at line 608
  5. markAsRawMode() disables further HTTP parsing on this connection
  6. Line 617 emits server.emit("upgrade", http_req, socket, kEmptyBuffer) — the HELLO bytes are dropped
  7. The upgrade event listener receives an empty buffer as head instead of Buffer("HELLO")
  8. Since the parser already consumed those bytes and raw mode is now active, HELLO is permanently lost

Impact

Any protocol that pipelines initial data with the HTTP upgrade request (which is common in WebSocket and other upgrade protocols) will silently lose those bytes. The existing regression test doesn't catch this because it sends the protocol payload only after receiving the 101 response.

Fix

Change line 617 from:

server.emit("upgrade", http_req, socket, kEmptyBuffer);

to:

server.emit("upgrade", http_req, socket, connectHead ?? kEmptyBuffer);

Comment on lines 608 to +614
} else if (is_upgrade) {
// Hand off the connection to userland, mirroring CONNECT handler.
// Enable streaming so socket.write() and socket.on("data") work.
socket[kEnableStreaming](true);
// Tell uWebSockets to stop parsing HTTP on this connection,
// switching to raw pass-through mode for bidirectional data.
socketHandle.markAsRawMode();

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.

🟡 When an upgrade request has a body (hasBody=true), handle.pause() at line 566 pauses the OS socket before markAsRawMode() at line 614 sets isConnectRequest=true. Later, resumeSocket() in Zig checks isConnectRequest() which is now true and returns early without calling us_socket_resume — the OS-level pause is never cleared, making the connection permanently unreadable. The CONNECT handler avoids this by returning at line 548 before handle.pause(). Fix: move the upgrade check before handle.pause() (like CONNECT) or explicitly resume after markAsRawMode().

Extended reasoning...

What the bug is

The new upgrade handler at lines 608-618 runs after the if (hasBody) { handle.pause(); } check at line 565-566. When an upgrade request includes a body (Content-Length > 0 or Transfer-Encoding present), the OS-level socket is paused via handle.pause()doPause()pauseSocket() (NodeHTTPResponse.zig:124). At this point, isConnectRequest() is still false, so the guard at line 126 passes and the OS socket is paused.

Then at line 614, socketHandle.markAsRawMode() sets isConnectRequest = true on the uWebSockets HttpResponseData.

Why the socket can never be unpaused

When userland calls socket.resume(), it flows through #resumeSocket()response.resume()doResume()resumeSocket() (NodeHTTPResponse.zig:133). At line 135, the guard checks this.raw_response.?.isConnectRequest() — which is now true — and returns early WITHOUT calling this.raw_response.?.resume(). The OS-level socket pause is never cleared. No data can be received on the connection.

Why the CONNECT handler is unaffected

The CONNECT handler correctly avoids this issue because it returns at line 548, before handle.pause() at line 565. The upgrade handler at line 608 runs after the pause, creating a window where the socket is OS-level paused and then markAsRawMode() prevents it from ever being unpaused.

Step-by-step proof with a concrete example

  1. Client sends: POST / HTTP/1.1\r\nHost: localhost\r\nUpgrade: custom-protocol\r\nConnection: Upgrade\r\nContent-Length: 5\r\n\r\nhello
  2. hasBody is true (Content-Length > 0), so line 566 calls handle.pause()pauseSocket() at Zig:124 → isConnectRequest() is false → OS socket is paused via us_socket_pause.
  3. is_upgrade is truthy ("custom-protocol"), entering the else if (is_upgrade) branch at line 608.
  4. Line 614: socketHandle.markAsRawMode() sets isConnectRequest = true.
  5. Server handler calls socket.resume()#resumeSocket()resumeSocket() at Zig:133 → guard at line 135 checks isConnectRequest()true → returns early. OS socket stays paused.
  6. The connection is permanently unreadable.

Impact

This is a nit/edge case because standard WebSocket upgrades use GET without a body (hasBody=false), so handle.pause() is never called and the socket works fine. Only custom protocol upgrades using POST/PUT with bodies would trigger this. However, for those cases, the connection becomes completely non-functional after the upgrade.

Suggested fix

Either (a) move the upgrade check before handle.pause() at line 565 — mirroring how the CONNECT handler returns at line 548 before the pause — or (b) explicitly resume the OS socket in the upgrade handler after markAsRawMode(), e.g. by calling handle.resume() before markAsRawMode() or directly after it.

@robobun

robobun commented May 14, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #30664 — same CONNECT-mirroring approach but keeps the diff to _http_server.ts + one markAsRawMode host function, drops the large bun-uws diff that's now the main conflict blocker here.

@robobun robobun closed this May 22, 2026
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.

node:http server upgrade socket does not hand off to userland — incoming data parsed as HTTP (400 Bad Request)

1 participant