Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
960e5db2fe fix: use hasContentLen instead of hasContentLength for header removal
Use the post-headers flag (hasContentLen) that reflects the effective
headers after writeHead() obj headers are applied, rather than the
initial hasContentLength value.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 18:15:20 +00:00
Claude Bot
7ab5cf7a34 fix(http): silently discard trailers when chunked encoding not used
Per Node.js documentation, `res.addTrailers()` should silently discard
trailers when chunked encoding is not enabled, rather than throwing an
error. This matches Node.js behavior where trailers set via addTrailers()
are only emitted if chunked encoding is used.

Previously, Bun threw ERR_HTTP_TRAILER_INVALID when trailers were set
via addTrailers() and chunked encoding was not enabled. Now we only
throw when the "Trailer" header is explicitly set (indicating intent
to send trailers), while silently discarding trailers set via
addTrailers() when chunked encoding is not in use.

Fixes #26171

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 17:57:57 +00:00
2 changed files with 143 additions and 10 deletions

View File

@@ -1177,17 +1177,30 @@ function _writeHead(statusCode, reason, obj, response) {
if (k) response.setHeader(k, obj[k]);
}
}
if (
(response.chunkedEncoding !== true || response.hasHeader("content-length")) &&
(response._trailer || response.hasHeader("trailer"))
) {
// remove the invalid content-length or trailer header
if (hasContentLength) {
response.removeHeader("trailer");
} else {
response.removeHeader("content-length");
// Check if chunked encoding is being used. Trailers require chunked encoding.
// Check both the chunkedEncoding property and the Transfer-Encoding header.
const teHeader = response.getHeader("transfer-encoding");
const isChunked = response.chunkedEncoding === true || (teHeader && /chunked/i.test(String(teHeader)));
const hasContentLen = response.hasHeader("content-length");
if (!isChunked || hasContentLen) {
// Trailers cannot be used without chunked encoding
if (response.hasHeader("trailer")) {
// If "Trailer" header is explicitly set, that's an error
// Use hasContentLen (not hasContentLength) to reflect current headers after obj applied
if (hasContentLen) {
response.removeHeader("trailer");
} else {
response.removeHeader("content-length");
}
throw $ERR_HTTP_TRAILER_INVALID("Trailers are invalid with this transfer encoding");
}
// If only _trailer is set (via addTrailers), silently discard per Node.js docs:
// "Trailers will only be emitted if chunked encoding is used for the response;
// if it is not (e.g. if the request was HTTP/1.0), they will be silently discarded."
if (response._trailer) {
response._trailer = "";
}
throw $ERR_HTTP_TRAILER_INVALID("Trailers are invalid with this transfer encoding");
}
}

View File

@@ -0,0 +1,120 @@
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/26171
// res.addTrailers() before res.writeHead() and res.end() should silently discard
// trailers when chunked encoding is not enabled, matching Node.js behavior.
// Direct test using node:http server - this is the exact reproduction from the issue
test("node:http server - addTrailers before writeHead should not throw", async () => {
const { createServer } = await import("node:http");
const server = createServer((req, res) => {
// Add trailers before writeHead - per Node.js docs, these should be silently discarded
// when chunked encoding is not used
res.addTrailers({ "Content-MD5": "7895bf4b8828b55ceaf47747b4bca667" });
res.writeHead(200);
res.end("OK");
});
await new Promise<void>(resolve => {
server.listen(0, () => resolve());
});
const port = (server.address() as any).port;
try {
const response = await fetch(`http://localhost:${port}`);
expect(response.status).toBe(200);
const text = await response.text();
expect(text).toBe("OK");
} finally {
server.close();
}
});
// Test with explicit Content-Length header
test("node:http server - addTrailers with Content-Length should be silently discarded", async () => {
const { createServer } = await import("node:http");
const server = createServer((req, res) => {
// Add trailers before writeHead - per Node.js docs, these should be silently discarded
res.addTrailers({ "Content-MD5": "7895bf4b8828b55ceaf47747b4bca667" });
res.writeHead(200, { "Content-Length": "2" });
res.end("OK");
});
await new Promise<void>(resolve => {
server.listen(0, () => resolve());
});
const port = (server.address() as any).port;
try {
const response = await fetch(`http://localhost:${port}`);
expect(response.status).toBe(200);
const text = await response.text();
expect(text).toBe("OK");
} finally {
server.close();
}
});
// Test that trailers work correctly with chunked encoding
test("node:http server - addTrailers with chunked encoding works", async () => {
const { createServer } = await import("node:http");
const server = createServer((req, res) => {
// When using chunked encoding (no Content-Length), trailers should work
res.writeHead(200, { "Transfer-Encoding": "chunked", "Trailer": "Content-MD5" });
res.write("Hello");
res.addTrailers({ "Content-MD5": "7895bf4b8828b55ceaf47747b4bca667" });
res.end();
});
await new Promise<void>(resolve => {
server.listen(0, () => resolve());
});
const port = (server.address() as any).port;
try {
const response = await fetch(`http://localhost:${port}`);
expect(response.status).toBe(200);
const text = await response.text();
expect(text).toBe("Hello");
} finally {
server.close();
}
});
// Test that explicit Trailer header without chunked encoding still throws
test("node:http server - explicit Trailer header with Content-Length throws", async () => {
const { createServer } = await import("node:http");
let errorCode: string | null = null;
const server = createServer((req, res) => {
// Setting the Trailer header explicitly should throw when not using chunked encoding
try {
res.writeHead(200, { "Content-Length": "2", "Trailer": "Content-MD5" });
res.end("OK");
} catch (e: any) {
errorCode = e.code;
// Can't call writeHead again after it threw, so just destroy the socket
res.socket?.destroy();
}
});
await new Promise<void>(resolve => {
server.listen(0, () => resolve());
});
const port = (server.address() as any).port;
try {
await fetch(`http://localhost:${port}`).catch(() => {});
// The error should have been caught
expect(errorCode).toBe("ERR_HTTP_TRAILER_INVALID");
} finally {
server.close();
}
});