Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/bun-uws/src/HttpResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,14 @@ struct HttpResponse : public AsyncSocket<SSL> {
return httpResponseData->isConnectRequest;
}

/* Mark this connection as a raw/tunnel connection (like CONNECT).
* This stops the HTTP parser from parsing subsequent data as HTTP,
* enabling raw bidirectional communication after protocol upgrade. */
void markAsRawMode() {
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();
httpResponseData->isConnectRequest = true;
}

void setWriteOffset(uint64_t offset) {
HttpResponseData<SSL> *httpResponseData = getHttpResponseData();

Expand Down
24 changes: 24 additions & 0 deletions src/bun.js/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "helpers.h"
#include <JavaScriptCore/JSCJSValueInlines.h>
#include <wtf/text/WTFString.h>
#include <bun-uws/src/App.h>

extern "C" EncodedJSValue us_socket_buffered_js_write(void* socket, bool is_ssl, bool ended, us_socket_stream_buffer_t* streamBuffer, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue data, JSC::EncodedJSValue encoding);
extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest, int* port, bool* is_ipv6);
Expand All @@ -28,6 +29,7 @@ JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterBytesWritten);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketClose);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketWrite);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketEnd);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketMarkAsRawMode);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterResponse);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterRemoteAddress);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterLocalAddress);
Expand Down Expand Up @@ -56,6 +58,7 @@ static const JSC::HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] =
{ "close"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketClose, 0 } },
{ "write"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketWrite, 2 } },
{ "end"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketEnd, 0 } },
{ "markAsRawMode"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketMarkAsRawMode, 0 } },
{ "secureEstablished"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterIsSecureEstablished, noOpSetter } },
};

Expand Down Expand Up @@ -113,6 +116,27 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketEnd, (JSC::JSGlobalObject
return JSValue::encode(JSC::jsUndefined());
}

JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketMarkAsRawMode, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto* thisObject = jsDynamicCast<JSNodeHTTPServerSocket*>(callFrame->thisValue());
if (!thisObject) [[unlikely]] {
return JSValue::encode(JSC::jsUndefined());
}
if (thisObject->isClosed()) {
return JSValue::encode(JSC::jsUndefined());
}
// Set isConnectRequest on the HttpResponseData so that the HTTP parser
// stops parsing subsequent data as HTTP and passes it through as raw data.
if (thisObject->is_ssl) {
auto* httpResponseData = reinterpret_cast<uWS::HttpResponseData<true>*>(us_socket_ext(true, thisObject->socket));
httpResponseData->isConnectRequest = true;
} else {
auto* httpResponseData = reinterpret_cast<uWS::HttpResponseData<false>*>(us_socket_ext(false, thisObject->socket));
httpResponseData->isConnectRequest = true;
}
return JSValue::encode(JSC::jsUndefined());
}

// Implementation of custom getters
JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
{
Expand Down
10 changes: 10 additions & 0 deletions src/deps/libuwsockets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1842,6 +1842,16 @@ __attribute__((callback (corker, ctx)))
return uwsRes->isConnectRequest();
}
}
void uws_res_mark_as_raw_mode(int ssl, uws_res_r res)
{
if (ssl) {
uWS::HttpResponse<true> *uwsRes = (uWS::HttpResponse<true> *)res;
uwsRes->markAsRawMode();
} else {
uWS::HttpResponse<false> *uwsRes = (uWS::HttpResponse<false> *)res;
uwsRes->markAsRawMode();
}
}
void *uws_res_get_native_handle(int ssl, uws_res_r res)
{
if (ssl)
Expand Down
13 changes: 13 additions & 0 deletions src/deps/uws/Response.zig
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ pub fn NewResponse(ssl_flag: i32) type {
return c.uws_res_is_connect_request(ssl_flag, res.downcast());
}

/// Mark this connection as raw/tunnel mode (like CONNECT).
/// Stops the HTTP parser from parsing subsequent data as HTTP.
pub fn markAsRawMode(res: *Response) void {
c.uws_res_mark_as_raw_mode(ssl_flag, res.downcast());
}

pub fn flushHeaders(res: *Response, flushImmediately: bool) void {
c.uws_res_flush_headers(ssl_flag, res.downcast(), flushImmediately);
}
Expand Down Expand Up @@ -590,6 +596,12 @@ pub const AnyResponse = union(enum) {
};
}

pub fn markAsRawMode(this: AnyResponse) void {
switch (this) {
inline else => |resp| resp.markAsRawMode(),
}
}

pub fn endStream(this: AnyResponse, close_connection: bool) void {
switch (this) {
inline else => |resp| resp.endStream(close_connection),
Expand Down Expand Up @@ -672,6 +684,7 @@ const c = struct {
pub extern fn us_socket_mark_needs_more_not_ssl(socket: ?*c.uws_res) void;
pub extern fn uws_res_state(ssl: c_int, res: *const c.uws_res) State;
pub extern fn uws_res_is_connect_request(ssl: i32, res: *c.uws_res) bool;
pub extern fn uws_res_mark_as_raw_mode(ssl: i32, res: *c.uws_res) void;
pub extern fn uws_res_get_remote_address_info(res: *c.uws_res, dest: *[*]const u8, port: *i32, is_ipv6: *bool) usize;
pub extern fn uws_res_uncork(ssl: i32, res: *c.uws_res) void;
pub extern fn uws_res_end(ssl: i32, res: *c.uws_res, data: [*c]const u8, length: usize, close_connection: bool) void;
Expand Down
17 changes: 9 additions & 8 deletions src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,15 +606,16 @@ Server.prototype[kRealListen] = function (tls, port, host, socketPath, reusePort
http_res.end();
socket.destroy();
} 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();
Comment on lines 608 to +614

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.

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;
Comment on lines 617 to +618

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 608 to +618

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

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);

} else if (http_req.headers.expect !== undefined) {
if (http_req.headers.expect === "100-continue") {
if (server.listenerCount("checkContinue") > 0) {
Expand Down
78 changes: 78 additions & 0 deletions test/regression/issue/28157.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";

test("node:http upgrade socket hands off to userland for bidirectional communication", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import http from "node:http";
import net from "node:net";

const server = http.createServer();

server.on("upgrade", (req, socket, head) => {
socket.write(
"HTTP/1.1 101 Switching Protocols\\r\\n" +
"Upgrade: custom\\r\\n" +
"Connection: Upgrade\\r\\n" +
"\\r\\n"
);

socket.on("data", (chunk) => {
socket.write("ECHO:" + chunk.toString());
});

socket.resume();
});

server.listen(0, "127.0.0.1", () => {
const port = server.address().port;

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"
);
Comment on lines +33 to +40

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.

});

let gotUpgrade = false;
let buf = "";

client.on("data", (chunk) => {
buf += chunk.toString();

if (!gotUpgrade && buf.includes("\\r\\n\\r\\n")) {
gotUpgrade = true;
client.write("hello from client");
}

if (buf.includes("ECHO:")) {
console.log(buf.substring(buf.indexOf("ECHO:")));
client.end();
server.close(() => process.exit(0));
}
});

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

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.

});
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).not.toContain("TIMEOUT");
expect(stdout.trim()).toBe("ECHO:hello from client");
expect(exitCode).toBe(0);
});
Loading