Summary
When a Worker makes an outbound fetch() whose request carries Expect: 100-continue and a body, workerd stalls for ~30 s (and on a current compatibility_date, indefinitely) before sending the body, then the request completes normally. Browser fetch and the Fetch standard do not block on the interim 100 Continue — they just send the body. workerd should match that.
Why this affects a lot of people
nodejs_compat makes libraries bundle their node build. AWS SDK JS v3's @aws-sdk/middleware-expect-continue adds Expect: 100-continue to bodied S3 PutObject/UploadPart requests, gated on runtime === "node":
if (HttpRequest.isInstance(request) && request.body && options.runtime === "node") {
request.headers = { ...request.headers, Expect: "100-continue" };
}
On Workers the node build runs over a fetch transport (@smithy/fetch-http-handler, since there is no Node http), so every S3 PUT hangs ~30 s. This is the exact stack Cloudflare's own R2 docs recommend (@aws-sdk/client-s3 + FetchHttpHandler), so it hits Workers + R2/S3 users broadly — in wrangler dev and in production. It's intermittent because a warm/reused connection sometimes skips the handshake ("works once or twice, then hangs").
Steps to reproduce
A Worker issuing sequential PutObjectCommands against R2 (or any S3 endpoint) with requestHandler: new FetchHttpHandler(). The first (cold-connection) bodied PUT stalls. A raw fetch() PUT to the same host is ~1 ms, so it is not the network:
| request |
time |
raw fetch() PUT (no SDK) |
~1 ms |
bodyless HeadBucket (no Expect) |
~15 ms |
SDK PutObject, Expect: 100-continue present (compat 2025-04-02) |
30,264 ms |
SDK PutObject, Expect present (compat 2026-06-23, workerd 1.20260623.1) |
>120,000 ms — never completed |
SDK PutObject, Expect header stripped |
~5 ms (MinIO) / ~200 ms (R2) |
Reproduces identically on wrangler dev (miniflare/workerd) and deployed Workers. Notably, on a current compatibility_date the stall is worse — the PUT did not complete within 120 s.
Expected behavior
Outbound fetch should follow browser/Fetch semantics for Expect: 100-continue: send the body without blocking on an interim 100 (browsers effectively ignore the header). The node:http docs already note Expect: 100-continue / the continue event are unsupported — but "unsupported" should mean ignored, not a multi-second (or unbounded) silent stall.
Workaround
Strip the (unsigned → signature-safe) Expect header before transmission. For AWS SDK JS v3:
client.middlewareStack.add(
(next) => (args) => {
const headers = args.request?.headers;
if (headers) {
for (const k of Object.keys(headers)) {
if (k.toLowerCase() === "expect") delete headers[k];
}
}
return next(args);
},
{ step: "finalizeRequest", name: "stripExpectContinue" }
);
Environment
- workerd
1.20260623.1 (and earlier); reproduced in wrangler dev and production
compatibility_date tested: 2025-04-02 (~30 s) and 2026-06-23 (>120 s); compatibility_flags: ["nodejs_compat"]
@aws-sdk/client-s3@3.873.0, @smithy/fetch-http-handler@5.x, @astrojs/cloudflare@13, Node 22 host
Summary
When a Worker makes an outbound
fetch()whose request carriesExpect: 100-continueand a body, workerd stalls for ~30 s (and on a currentcompatibility_date, indefinitely) before sending the body, then the request completes normally. Browserfetchand the Fetch standard do not block on the interim100 Continue— they just send the body. workerd should match that.Why this affects a lot of people
nodejs_compatmakes libraries bundle their node build. AWS SDK JS v3's@aws-sdk/middleware-expect-continueaddsExpect: 100-continueto bodied S3PutObject/UploadPartrequests, gated onruntime === "node":On Workers the node build runs over a
fetchtransport (@smithy/fetch-http-handler, since there is no Nodehttp), so every S3 PUT hangs ~30 s. This is the exact stack Cloudflare's own R2 docs recommend (@aws-sdk/client-s3+FetchHttpHandler), so it hits Workers + R2/S3 users broadly — inwrangler devand in production. It's intermittent because a warm/reused connection sometimes skips the handshake ("works once or twice, then hangs").Steps to reproduce
A Worker issuing sequential
PutObjectCommands against R2 (or any S3 endpoint) withrequestHandler: new FetchHttpHandler(). The first (cold-connection) bodied PUT stalls. A rawfetch()PUT to the same host is ~1 ms, so it is not the network:fetch()PUT (no SDK)HeadBucket(noExpect)PutObject,Expect: 100-continuepresent (compat2025-04-02)PutObject,Expectpresent (compat2026-06-23, workerd1.20260623.1)PutObject,Expectheader strippedReproduces identically on
wrangler dev(miniflare/workerd) and deployed Workers. Notably, on a currentcompatibility_datethe stall is worse — the PUT did not complete within 120 s.Expected behavior
Outbound
fetchshould follow browser/Fetch semantics forExpect: 100-continue: send the body without blocking on an interim100(browsers effectively ignore the header). Thenode:httpdocs already noteExpect: 100-continue/ thecontinueevent are unsupported — but "unsupported" should mean ignored, not a multi-second (or unbounded) silent stall.Workaround
Strip the (unsigned → signature-safe)
Expectheader before transmission. For AWS SDK JS v3:Environment
1.20260623.1(and earlier); reproduced inwrangler devand productioncompatibility_datetested:2025-04-02(~30 s) and2026-06-23(>120 s);compatibility_flags: ["nodejs_compat"]@aws-sdk/client-s3@3.873.0,@smithy/fetch-http-handler@5.x,@astrojs/cloudflare@13, Node 22 host