mirror of
https://github.com/oven-sh/bun
synced 2026-02-15 13:22:07 +00:00
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>
This commit is contained in:
@@ -1177,17 +1177,29 @@ 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
|
||||
if (hasContentLength) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
120
test/regression/issue/26171.test.ts
Normal file
120
test/regression/issue/26171.test.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user