cloudflare / cloudflare/workerd

Outbound fetch stalls ~30s on requests with Expect: 100-continue instead of sending the body (breaks AWS SDK S3 PutObject under nodejs_compat)

Open
#6,845 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
8.7k
Forks
739
Avg merge
2d 20h
Merged PRs (30d)
174

Description

### 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`](https://github.com/aws/aws-sdk-js-v3/tree/main/packages/middleware-expect-continue) adds `Expect: 100-continue` to bodied S3 `PutObject`/`UploadPart` requests, gated on `runtime === "node"`:

```js
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](https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js-v3/) (`@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 `PutObjectCommand`s 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](https://developers.cloudflare.com/workers/runtime-apis/nodejs/http/) 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:

```js
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

Contributor guide

Open the contributing guide

Research direction

Start with the outbound fetch path in workerd and reproduce the sequential S3 PutObject request using @smithy/fetch-http-handler with an Expect: 100-continue header and body. Compare it with raw fetch and the header-stripped workaround described in the issue. Done means bodied requests send their body without waiting for an interim 100 response, including under nodejs_compat, with regression coverage for the reported stall.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, cpp, javascript, nodejs
Domain
backend, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.