diff --git a/src/workerd/api/global-scope.c++ b/src/workerd/api/global-scope.c++ index 2a98eb1f8a9..898ffd8761f 100644 --- a/src/workerd/api/global-scope.c++ +++ b/src/workerd/api/global-scope.c++ @@ -248,6 +248,23 @@ kj::Promise> ServiceWorkerGlobalScope::request( body = Body::ExtractedBody(jsStream.addRef()); } + // If the request doesn't specify "Content-Length" or "Transfer-Encoding", set "Content-Length" + // to the body length if it's known. This ensures handlers for worker-to-worker requests can + // access known body lengths if they're set, without buffering bodies. + if (body != nullptr && + headers.get(kj::HttpHeaderId::CONTENT_LENGTH) == nullptr && + headers.get(kj::HttpHeaderId::TRANSFER_ENCODING) == nullptr) { + // We can't use headers.set() here as headers is marked const. Instead, we call set() on the + // JavaScript headers object, ignoring the REQUEST guard that usually makes them immutable. + KJ_IF_MAYBE(l, requestBody.tryGetLength()) { + jsHeaders->setUnguarded(jsg::ByteString(kj::str("Content-Length")), + jsg::ByteString(kj::str(*l))); + } else { + jsHeaders->setUnguarded(jsg::ByteString(kj::str("Transfer-Encoding")), + jsg::ByteString(kj::str("chunked"))); + } + } + auto jsRequest = jsg::alloc( method, url, Request::Redirect::MANUAL, kj::mv(jsHeaders), jsg::alloc(IoContext::NEXT_CLIENT_CHANNEL, diff --git a/src/workerd/api/http-test.js b/src/workerd/api/http-test.js index 0a34a47cc01..31466a6a7b9 100644 --- a/src/workerd/api/http-test.js +++ b/src/workerd/api/http-test.js @@ -7,12 +7,50 @@ import assert from "node:assert"; let scheduledLastCtrl; export default { + async fetch(request, env, ctx) { + const { pathname } = new URL(request.url); + if (pathname === "/body-length") { + return Response.json(Object.fromEntries(request.headers)); + } + return new Response(null, { status: 404 }); + }, + async scheduled(ctrl, env, ctx) { scheduledLastCtrl = ctrl; if (ctrl.cron === "* * * * 30") ctrl.noRetry(); }, async test(ctrl, env, ctx) { + // Call `fetch()` with known body length + { + const body = new FixedLengthStream(3); + const writer = body.writable.getWriter(); + void writer.write(new Uint8Array([1, 2, 3])); + void writer.close(); + const response = await env.SERVICE.fetch("http://placeholder/body-length", { + method: "POST", + body: body.readable, + }); + const headers = new Headers(await response.json()); + assert.strictEqual(headers.get("Content-Length"), "3"); + assert.strictEqual(headers.get("Transfer-Encoding"), null); + } + + // Check `fetch()` with unknown body length + { + const body = new IdentityTransformStream(); + const writer = body.writable.getWriter(); + void writer.write(new Uint8Array([1, 2, 3])); + void writer.close(); + const response = await env.SERVICE.fetch("http://placeholder/body-length", { + method: "POST", + body: body.readable, + }); + const headers = new Headers(await response.json()); + assert.strictEqual(headers.get("Content-Length"), null); + assert.strictEqual(headers.get("Transfer-Encoding"), "chunked"); + } + // Call `scheduled()` with no options { const result = await env.SERVICE.scheduled(); diff --git a/src/workerd/api/http.c++ b/src/workerd/api/http.c++ index 4e1055979b2..caac5a6c266 100644 --- a/src/workerd/api/http.c++ +++ b/src/workerd/api/http.c++ @@ -284,6 +284,10 @@ bool Headers::has(jsg::ByteString name) { void Headers::set(jsg::ByteString name, jsg::ByteString value) { checkGuard(); + setUnguarded(kj::mv(name), kj::mv(value)); +} + +void Headers::setUnguarded(jsg::ByteString name, jsg::ByteString value) { requireValidHeaderName(name); auto key = toLower(name); value = normalizeHeaderValue(kj::mv(value)); diff --git a/src/workerd/api/http.h b/src/workerd/api/http.h index b5c9f9a52b4..0d9573b2e92 100644 --- a/src/workerd/api/http.h +++ b/src/workerd/api/http.h @@ -95,6 +95,9 @@ class Headers: public jsg::Object { // is not permitted to be combined into a single instance. bool has(jsg::ByteString name); void set(jsg::ByteString name, jsg::ByteString value); + void setUnguarded(jsg::ByteString name, jsg::ByteString value); + // Like set(), but ignores the header guard if set. This can only be called from C++, and may be + // used to mutate headers before dispatching a request. void append(jsg::ByteString name, jsg::ByteString value); void delete_(jsg::ByteString name); void forEach(jsg::Lock& js,