Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
ce7394ea09 fix(http): preserve Content-Length header for streaming request bodies
When a user explicitly provides a Content-Length header with a streaming
body (async generator or ReadableStream), Bun now respects it instead of
replacing it with Transfer-Encoding: chunked.

This is required for services like Azure Storage that compute authentication
signatures (HMAC) based on the Content-Length header value. When Bun replaced
the user-provided Content-Length with chunked encoding, the server's computed
signature wouldn't match, causing authentication failures.

Fixes #18854

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 07:37:49 +00:00
2 changed files with 115 additions and 1 deletions

View File

@@ -719,7 +719,16 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
if (body_len > 0 or this.method.hasRequestBody()) {
if (this.flags.is_streaming_request_body) {
if (add_transfer_encoding and this.flags.upgrade_state == .none) {
// If user explicitly provided Content-Length, use it instead of chunked encoding.
// This is required for services like Azure Storage that compute authentication
// signatures based on the Content-Length header value.
if (original_content_length) |content_length| {
request_headers_buf[header_count] = .{
.name = content_length_header_name,
.value = content_length,
};
header_count += 1;
} else if (add_transfer_encoding and this.flags.upgrade_state == .none) {
request_headers_buf[header_count] = chunked_encoded_header;
header_count += 1;
}

View File

@@ -0,0 +1,105 @@
import { expect, test } from "bun:test";
// Test for GitHub issue #18854
// Azure Storage file uploads fail because Bun strips user-provided Content-Length
// headers on streaming bodies and replaces them with Transfer-Encoding: chunked
test("fetch should preserve Content-Length header when explicitly provided with streaming body", async () => {
const bodyData = "x".repeat(100000); // >64KB to ensure it's a meaningful test
const contentLength = Buffer.byteLength(bodyData);
// Create a streaming body using an async generator
async function* streamBody() {
yield bodyData;
}
let receivedHeaders: Headers | null = null;
using server = Bun.serve({
port: 0,
async fetch(req) {
receivedHeaders = req.headers;
await req.text(); // consume body
return new Response("ok");
},
});
await fetch(`http://localhost:${server.port}/test`, {
method: "POST",
headers: {
"Content-Length": String(contentLength),
},
body: streamBody() as any,
duplex: "half",
});
// Verify Content-Length is preserved and Transfer-Encoding is not used
expect(receivedHeaders?.get("content-length")).toBe(String(contentLength));
expect(receivedHeaders?.get("transfer-encoding")).toBeNull();
});
test("fetch should preserve Content-Length header with ReadableStream body", async () => {
const bodyData = "y".repeat(50000);
const contentLength = Buffer.byteLength(bodyData);
// Create a ReadableStream body
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(bodyData));
controller.close();
},
});
let receivedHeaders: Headers | null = null;
using server = Bun.serve({
port: 0,
async fetch(req) {
receivedHeaders = req.headers;
await req.text(); // consume body
return new Response("ok");
},
});
await fetch(`http://localhost:${server.port}/test`, {
method: "POST",
headers: {
"Content-Length": String(contentLength),
},
body: stream,
duplex: "half",
});
// Verify Content-Length is preserved and Transfer-Encoding is not used
expect(receivedHeaders?.get("content-length")).toBe(String(contentLength));
expect(receivedHeaders?.get("transfer-encoding")).toBeNull();
});
test("fetch should use chunked encoding when Content-Length is not provided for streaming body", async () => {
const bodyData = "z".repeat(10000);
async function* streamBody() {
yield bodyData;
}
let receivedHeaders: Headers | null = null;
using server = Bun.serve({
port: 0,
async fetch(req) {
receivedHeaders = req.headers;
await req.text(); // consume body
return new Response("ok");
},
});
await fetch(`http://localhost:${server.port}/test`, {
method: "POST",
body: streamBody() as any,
duplex: "half",
});
// Without explicit Content-Length, chunked encoding should be used
expect(receivedHeaders?.get("transfer-encoding")).toBe("chunked");
expect(receivedHeaders?.get("content-length")).toBeNull();
});