Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
e0070ae192 fix: forward options in TestContext.test() and adjust test timeout
Address review feedback:
- Forward options (including timeout) to bun:test in all
  TestContext.test() branches (only, todo, skip, default)
- Reduce outer test timeout from 30s to 15s with explanatory comment

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-25 16:24:55 +00:00
Claude Bot
e1140dd834 fix(test): use no-timeout default for node:test to match Node.js semantics
Node.js's `node:test` defaults to `Infinity` timeout (tests never time
out), but Bun was applying its own 5000ms default. This caused async
tests taking longer than 5s to fail unexpectedly.

Two changes:
- Set timeout=0 (no timeout) as the default for node:test when the user
  hasn't specified one, matching Node.js behavior.
- Replace the done-callback wrapper with an async function to avoid
  bun:test misinterpreting the wrapper's `done` parameter as a
  done-callback style test, which caused misleading error messages.

Closes #27422

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-25 16:12:51 +00:00
Martin Amps
38e4340d28 fix(http): prevent duplicate Transfer-Encoding header in node:http (#27398)
### What does this PR do?

Fixes https://github.com/oven-sh/bun/issues/21201. Ran into this today
during a migration from node->bun. TLDR if you set `Transfer-Encoding:
chunked` via `res.writeHead()`, Bun sends the header twice because two
layers independently add it:

1. `writeFetchHeadersToUWSResponse` (NodeHTTP.cpp) writes the user's
headers to the response buffer
2. uWS's `HttpResponse::write()` auto-inserts `Transfer-Encoding:
chunked` since no flag indicates it was already set

Added a `HTTP_WROTE_TRANSFER_ENCODING_HEADER` flag copying the pattern
for `Content-Length` and `Date` deduping, checked before auto-insertion
in `write()`, `flushHeaders()`, and `sendTerminatingChunk()`.

### How did you verify your code works?

Minimal repro:

```
import http from "node:http";
import net from "node:net";

http.createServer((_, res) => {
  res.writeHead(200, { "Transfer-Encoding": "chunked" });
  res.write("ok");
}).listen(0, "127.0.0.1", function () {
  const s = net.createConnection(this.address().port, "127.0.0.1", () =>
    s.write("GET / HTTP/1.1\r\nHost: x\r\n\r\n"));
  s.on("data", (d) => {
    console.log(d.toString().split("\r\n\r\n")[0]);
    s.destroy(); this.close();
  });
});
```

Node working (showing header once)

```
$ node --version
v22.21.1

$ node /Users/mamps/code/labs/conway/tmp/bun-duplicate-te.mjs
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Tue, 24 Feb 2026 06:14:50 GMT
Connection: keep-alive
Keep-Alive: timeout=5
```

Bun bug (duplicate header):
```
$ bun --version
1.3.9

$ bun bun-duplicate-te.mjs
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Tue, 24 Feb 2026 06:13:55 GMT
Transfer-Encoding: chunked
```

Bun fixed:
```
$ ./build/debug/bun-debug --version
1.3.10-debug

$ BUN_DEBUG_QUIET_LOGS=1 ./build/debug/bun-debug /tmp/bun-duplicate-te.mjs
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Tue, 24 Feb 2026 06:15:53 GMT
```
2026-02-23 23:04:19 -08:00
robobun
6b1d6c769b fix(sys): remove MSG_NOSIGNAL from recvfrom flags (#27390)
## Summary

- `MSG_NOSIGNAL` is only valid for send operations (`send`, `sendto`,
`sendmsg`), not receive operations (`recv`, `recvfrom`, `recvmsg`).
Passing it to `recvfrom` causes `EINVAL` in strict environments like
gVisor (Google Cloud Run).
- Split the shared `socket_flags_nonblock` constant into
`recv_flags_nonblock` (`MSG_DONTWAIT` only) and `send_flags_nonblock`
(`MSG_DONTWAIT | MSG_NOSIGNAL`).

Closes #27389

## Test plan

- [x] Added regression test `test/regression/issue/27389.test.ts` that
exercises the socket recv path
- [x] Debug build passes (`bun bd test
test/regression/issue/27389.test.ts`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-23 18:24:37 -08:00
10 changed files with 171 additions and 36 deletions

View File

@@ -462,7 +462,7 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
#ifdef _WIN32
const int recv_flags = MSG_PUSH_IMMEDIATE;
#else
const int recv_flags = MSG_DONTWAIT | MSG_NOSIGNAL;
const int recv_flags = MSG_DONTWAIT;
#endif
int length;

View File

@@ -467,7 +467,9 @@ public:
/* Write mark on first call to write */
writeMark();
writeHeader("Transfer-Encoding", "chunked");
if (!(httpResponseData->state & HttpResponseData<SSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER)) {
writeHeader("Transfer-Encoding", "chunked");
}
Super::write("\r\n", 2);
httpResponseData->state |= HttpResponseData<SSL>::HTTP_WRITE_CALLED;
}
@@ -489,7 +491,9 @@ public:
/* Write mark on first call to write */
writeMark();
writeHeader("Transfer-Encoding", "chunked");
if (!(httpResponseData->state & HttpResponseData<SSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER)) {
writeHeader("Transfer-Encoding", "chunked");
}
Super::write("\r\n", 2);
httpResponseData->state |= HttpResponseData<SSL>::HTTP_WRITE_CALLED;
}
@@ -558,7 +562,9 @@ public:
/* Write mark on first call to write */
writeMark();
writeHeader("Transfer-Encoding", "chunked");
if (!(httpResponseData->state & HttpResponseData<SSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER)) {
writeHeader("Transfer-Encoding", "chunked");
}
Super::write("\r\n", 2);
httpResponseData->state |= HttpResponseData<SSL>::HTTP_WRITE_CALLED;
}

View File

@@ -87,6 +87,7 @@ struct HttpResponseData : AsyncSocketData<SSL>, HttpParser {
HTTP_CONNECTION_CLOSE = 16, // used
HTTP_WROTE_CONTENT_LENGTH_HEADER = 32, // used
HTTP_WROTE_DATE_HEADER = 64, // used
HTTP_WROTE_TRANSFER_ENCODING_HEADER = 128, // used
};
/* Shared context pointer */

View File

@@ -570,6 +570,11 @@ static void writeFetchHeadersToUWSResponse(WebCore::FetchHeaders& headers, uWS::
if (header.key == WebCore::HTTPHeaderName::Date) {
data->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_DATE_HEADER;
}
// Prevent automatic Transfer-Encoding: chunked insertion when user provides one
if (header.key == WebCore::HTTPHeaderName::TransferEncoding) {
data->state |= uWS::HttpResponseData<isSSL>::HTTP_WROTE_TRANSFER_ENCODING_HEADER;
}
writeResponseHeader<isSSL>(res, name, value);
}
@@ -642,6 +647,7 @@ static void NodeHTTPServer__writeHead(
String key = propertyNames[i].string();
String value = headerValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, void());
writeResponseHeader<isSSL>(response, key, value);
}
}

View File

@@ -149,13 +149,13 @@ class TestContext {
const { test } = bunTest();
if (options.only) {
test.only(name, fn);
test.only(name, fn, options);
} else if (options.todo) {
test.todo(name, fn);
test.todo(name, fn, options);
} else if (options.skip) {
test.skip(name, fn);
test.skip(name, fn, options);
} else {
test(name, fn);
test(name, fn, options);
}
}
@@ -304,32 +304,25 @@ function createTest(arg0: unknown, arg1: unknown, arg2: unknown) {
checkNotInsideTest(ctx, "test");
const context = new TestContext(true, name, Bun.main, ctx);
const runTest = (done: (error?: unknown) => void) => {
// Return an async function instead of a done-callback style function.
// Using (done) => {} would cause bun:test to interpret the function as
// a done-callback test (because callback.length >= 1), leading to
// misleading "done callback" error messages on timeout.
const runTest = async () => {
const originalContext = ctx;
ctx = context;
const endTest = (error?: unknown) => {
try {
done(error);
} finally {
ctx = originalContext;
}
};
let result: unknown;
try {
result = fn(context);
} catch (error) {
endTest(error);
return;
}
if (result instanceof Promise) {
(result as Promise<unknown>).then(() => endTest()).catch(error => endTest(error));
} else {
endTest();
await fn(context);
} finally {
ctx = originalContext;
}
};
return { name, options, fn: runTest };
// Node.js node:test defaults to Infinity timeout (no timeout).
// In bun:test, timeout=0 means "no timeout", so use that as default.
const testOptions = { ...options, timeout: options.timeout ?? 0 };
return { name, options: testOptions, fn: runTest };
}
function createDescribe(arg0: unknown, arg1: unknown, arg2: unknown) {

View File

@@ -2035,10 +2035,10 @@ pub fn readAll(fd: bun.FileDescriptor, buf: []u8) Maybe(usize) {
return .{ .result = total_read };
}
const socket_flags_nonblock = c.MSG_DONTWAIT | c.MSG_NOSIGNAL;
const send_flags_nonblock = c.MSG_DONTWAIT | c.MSG_NOSIGNAL;
pub fn recvNonBlock(fd: bun.FileDescriptor, buf: []u8) Maybe(usize) {
return recv(fd, buf, socket_flags_nonblock);
return recv(fd, buf, recv_flags_nonblock);
}
pub fn poll(fds: []std.posix.pollfd, timeout: i32) Maybe(usize) {
@@ -2119,7 +2119,7 @@ pub fn kevent(fd: bun.FileDescriptor, changelist: []const std.c.Kevent, eventlis
}
pub fn sendNonBlock(fd: bun.FileDescriptor, buf: []const u8) Maybe(usize) {
return send(fd, buf, socket_flags_nonblock);
return send(fd, buf, send_flags_nonblock);
}
pub fn send(fd: bun.FileDescriptor, buf: []const u8, flag: u32) Maybe(usize) {
@@ -4359,11 +4359,13 @@ const bun = @import("bun");
const Environment = bun.Environment;
const FD = bun.FD;
const MAX_PATH_BYTES = bun.MAX_PATH_BYTES;
const c = bun.c; // translated c headers
const jsc = bun.jsc;
const libc_stat = bun.Stat;
const darwin_nocancel = bun.darwin.nocancel;
const c = bun.c; // translated c headers
const recv_flags_nonblock = c.MSG_DONTWAIT;
const windows = bun.windows;
const kernel32 = bun.windows.kernel32;
const ntdll = bun.windows.ntdll;

View File

@@ -1,10 +1,10 @@
import { test } from "bun:test";
import { expect, test } from "bun:test";
import { once } from "events";
import { request } from "http";
import { AddressInfo, Server } from "net";
import { createServer, request } from "http";
import { AddressInfo, connect, Server } from "net";
const fixture = "node-http-transfer-encoding-fixture.ts";
test(`should not duplicate transfer-encoding header`, async () => {
test(`should not duplicate transfer-encoding header in request`, async () => {
const { resolve, promise } = Promise.withResolvers();
const tcpServer = new Server();
tcpServer.listen(0, "127.0.0.1");
@@ -54,3 +54,43 @@ test(`should not duplicate transfer-encoding header`, async () => {
return promise;
});
test("should not duplicate transfer-encoding header in response when explicitly set", async () => {
await using server = createServer((req, res) => {
res.writeHead(200, { "Transfer-Encoding": "chunked" });
res.write("Hello, World!");
res.end("Goodbye, World!");
});
await once(server.listen(0, "127.0.0.1"), "listening");
const { port } = server.address() as AddressInfo;
const { promise, resolve, reject } = Promise.withResolvers<string>();
const socket = connect(port, "127.0.0.1", () => {
socket.write("GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n");
});
let rawResponse = "";
socket.on("data", (chunk: Buffer) => {
rawResponse += chunk.toString();
});
socket.on("end", () => resolve(rawResponse));
socket.on("error", reject);
const response = await promise;
const headerSection = response.split("\r\n\r\n")[0];
const headerLines = headerSection
.split("\r\n")
.slice(1) // Skip status line
.filter(line => line.length > 0);
const transferEncodingHeaders = headerLines.filter(line => line.toLowerCase().startsWith("transfer-encoding:"));
expect(transferEncodingHeaders).toHaveLength(1);
// Verify the body content is correctly delivered via chunked encoding
const bodySection = response.split("\r\n\r\n").slice(1).join("\r\n\r\n");
expect(bodySection).toContain("Hello, World!");
expect(bodySection).toContain("Goodbye, World!");
});

View File

@@ -0,0 +1,57 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// Regression test for #27389: recvfrom() was called with MSG_NOSIGNAL which
// is only valid for send operations. This caused EINVAL in strict environments
// like gVisor (Google Cloud Run). The fix removes MSG_NOSIGNAL from recv flags.
//
// On standard Linux the kernel silently ignores the invalid flag, so we verify
// the fix by ensuring socket recv operations complete without error.
test("socket recv works without EINVAL from invalid flags", async () => {
// Start a simple echo server and client that exercises the recv path
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const server = Bun.listen({
hostname: "127.0.0.1",
port: 0,
socket: {
open(socket) {},
data(socket, data) {
// Echo back the data
socket.write(data);
socket.end();
},
},
});
const client = await Bun.connect({
hostname: "127.0.0.1",
port: server.port,
socket: {
open(socket) {
socket.write("hello");
},
data(socket, data) {
console.log(Buffer.from(data).toString());
socket.end();
},
close() {
server.stop(true);
},
},
});
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("hello");
expect(exitCode).toBe(0);
});

View File

@@ -0,0 +1,7 @@
import { it } from "node:test";
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
it("async test exceeding default bun timeout", async () => {
await sleep(7000);
});

View File

@@ -0,0 +1,23 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
test("node:test async tests should not time out by default", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "test", "--timeout", "5000", import.meta.dir + "/27422-fixture.test.mjs"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const output = stdout + stderr;
// The test should pass because node:test defaults to no timeout,
// even though bun:test's default is 5000ms.
expect(output).toContain("1 pass");
expect(output).toContain("0 fail");
// Should not contain the misleading "done callback" error message
expect(output).not.toContain("done callback");
expect(exitCode).toBe(0);
// The spawned test sleeps for 7s, so this outer bun:test needs a longer timeout.
}, 15_000);