From 7ab5cf7a34b7782812632e8d34b3211888617ee0 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 16 Jan 2026 17:57:57 +0000 Subject: [PATCH] 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 --- src/js/node/_http_server.ts | 32 +++++--- test/regression/issue/26171.test.ts | 120 ++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 test/regression/issue/26171.test.ts diff --git a/src/js/node/_http_server.ts b/src/js/node/_http_server.ts index ae646dcd41..d45cc321e3 100644 --- a/src/js/node/_http_server.ts +++ b/src/js/node/_http_server.ts @@ -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"); } } diff --git a/test/regression/issue/26171.test.ts b/test/regression/issue/26171.test.ts new file mode 100644 index 0000000000..b2ba4799ac --- /dev/null +++ b/test/regression/issue/26171.test.ts @@ -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(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(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(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(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(); + } +});