Skip to content

fix(s3): send 100 Continue for empty-body Expect requests - #70

Open
kylecannon wants to merge 1 commit into
tyrchen:masterfrom
kylecannon:fix/s3-expect-continue-empty-body
Open

kylecannon wants to merge 1 commit into
tyrchen:masterfrom
kylecannon:fix/s3-expect-continue-empty-body

Conversation

@kylecannon

@kylecannon kylecannon commented Oct 30, 2026 •

Copy link
Copy Markdown

Summary

The Node AWS SDK v3 (@smithy/node-http-handler 2.x) sends Expect: 100-continue even on an empty PutObject, then waits for the 100 before finishing the request. Real S3 sends the 100 (checked against a us-west-2 bucket), and so does LocalStack, so against rustack the upload just sits there until the client's request timeout.

hyper skips the 100 when the body is empty, and a service can't send a 1xx itself, so this adds a small IO wrapper around each accepted connection. It follows HTTP/1 request and response framing, and when an empty-body request asked for a 100 it writes HTTP/1.1 100 Continue right before hyper's response.

It handles one request at a time per connection. If a client pipelines, or anything looks unusual (HTTP/2, upgrades, close-delimited responses, heads it can't parse), the wrapper steps aside for the rest of the connection and it behaves exactly as before.

What

  • apps/rustack/src/expect_continue.rs (new): the ExpectContinueIo wrapper and its tests.
  • apps/rustack/src/main.rs: wraps each accepted stream in ExpectContinueIo before handing it to hyper.
  • Cargo.toml, apps/rustack/Cargo.toml, Cargo.lock: adds httparse for head parsing.

Testing

  • Unit tests in apps/rustack/src/expect_continue.rs cover keep-alive, chunked bodies, split heads, pipelining, backpressure and HTTP/2 pass-through; cargo test -p rustack-cli passes.
  • Empty-body PutObject from @aws-sdk/client-s3 with the 2.x handler: hung until the 15 s request timeout before, 3 ms after.

The Node AWS SDK v3 (@smithy/node-http-handler 2.x) sends
`Expect: 100-continue` even on an empty PutObject, then waits for the
100 before finishing the request. Real S3 sends it, so you never see
this in production, but against rustack the upload just sits there
until the request timeout (5 minutes in our case).

hyper skips the 100 when the body is empty, and there's no way to send
a 1xx from a service, so this adds a small IO wrapper around each
accepted connection. It follows HTTP/1 request and response framing
and, when an empty-body request asks for a 100, writes one right before
hyper's response.

It handles one request at a time per connection. If a client pipelines,
or anything looks unusual (HTTP/2, upgrades, close-delimited responses,
heads it can't parse), the connection is left alone and behaves exactly
as before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant