Skip to content

Add Body type and make serve() body-generic for http@1 migration#4564

Draft
drganjoo wants to merge 6 commits intomainfrom
fahadzub/http1x-body
Draft

Add Body type and make serve() body-generic for http@1 migration#4564
drganjoo wants to merge 6 commits intomainfrom
fahadzub/http1x-body

Conversation

@drganjoo
Copy link
Copy Markdown
Contributor

Add Body type and make serve() body-generic for http@1 migration

Background: http@0 -> http@1 body model

In hyper 0.x (http@0), hyper::Body was a concrete type — every handler, middleware, and route used the same hyper::Body for both requests and responses. Simple, but inflexible.

In hyper 1.x (http@1), http_body::Body is a trait. hyper's own request body is now hyper::body::Incoming — a type that only hyper can construct. You cannot create an Incoming from bytes, a stream, or any other source — it is produced exclusively by hyper when reading from the wire. This is a fundamental shift: there is no single concrete body type anymore. Instead, the ecosystem expects routers, middleware, and Tower layers to be generic over B: http_body::Body. This is the right design — middleware like logging, compression, or metrics shouldn't care about the concrete body type. They work through the trait.

Our existing Route<B> is already generic over the body type with B = Incoming as the default. Codegen produces handlers that are body-generic. The serve() function and BoxBody/BoxBodySync response types were added as part of the http@1 merge. This PR does not change any of that.

The problem this PR solves

While body-generic middleware is the correct long-term design, teams writing application-level middleware (not reusable library middleware) face a practical pain point: threading B: http_body::Body<Data = Bytes> + Send + 'static through every service wrapper, plugin, and layer is verbose and error-prone. When a team just needs to stash a copy of the request body in http::Extensions, or reconstruct a request from buffered bytes, they need a concrete body type they can name and construct.

Incoming can't fill this role — you can't construct one from bytes, and it doesn't implement From<Vec<u8>> or similar. In the http@0 world, hyper::Body::from("hello") just worked. In http@1, there's a gap.

What this PR adds

1. Body — an opt-in, type-erased request body

pub struct Body(UnsyncBoxBody<Bytes, Error>);
  • Wraps any http_body::Body<Data = Bytes> behind one heap allocation
  • From<Incoming> — converts hyper's request body at the serve boundary
  • From<&str>, From<String>, From<Bytes>, From<Vec<u8>>, etc. — constructable from common types
  • Body::empty(), Default — for empty bodies
  • Implements http_body::Body itself, so it works anywhere the trait is expected
  • try_downcast avoids double-boxing when wrapping a Body in a Body

Teams that want simpler middleware signatures can use Route<Body> and let serve() handle the Incoming -> Body conversion. Teams that want zero-overhead can continue using Route<Incoming> — nothing changes for them.

2. serve() made body-generic via ReqBody: From<Incoming>

Previously, serve() hardcoded Incoming as the request body type. Now:

pub fn serve<L, M, S, ReqBody, RespBody>(listener: L, make_service: M) -> Serve<...>
where
    ReqBody: From<Incoming> + Send + 'static,
    // ...

A thin MapRequestBody adapter sits at the hyper boundary and calls req.map(ReqBody::from) on each incoming request. When ReqBody = Incoming, From<Incoming> for Incoming is the identity — zero cost. When ReqBody = Body, it performs one heap allocation via From<Incoming> for Body.

3. B renamed to RespBody throughout serve/mod.rs for clarity — B was ambiguous now that we have both request and response body generics.

4. Test coverage including streaming and error scenarios:

  • test_serve_with_custom_request_body_typeIncoming -> Body conversion works for buffered requests
  • test_serve_with_streaming_request_body — Chunked Transfer-Encoding streams through Body frame-by-frame
  • test_serve_with_streaming_response_bodywrap_stream response body arrives correctly through hyper
  • test_serve_with_streaming_response_body_error — Stream error mid-response propagates to client (not swallowed)
  • test_serve_with_streaming_request_body_error — Malformed chunked request doesn't panic or hang the server

Design principles preserved

  • Middleware should be generic over http_body::Body — this has not changed. Body is a convenience for application code, not a replacement for the trait.
  • BoxBody / BoxBodySync remain the response body typesBody is request-side only.
  • wrap_stream / wrap_stream_sync remain the stream-to-body APIs — no redundant alternatives added.
  • No codegen changes — generated routers and handlers are unaffected.
  • No breaking changes — existing callers of serve() with Service<Request<Incoming>> continue to work via type inference.

Changes

  • src/body.rs — Add Body, BodyDataStream, From impls, remove unnecessary private StreamBody and SyncWrapper
  • src/serve/mod.rs — Add ReqBody generic, MapRequestBody adapter, rename B -> RespBody
  • tests/serve_integration_test.rs — +5 new tests, remove flaky sleeps, harden assertions
  • tests/graceful_shutdown_test.rs — Harden assertions, fix unwrap -> expect

Test plan

  • cargo test — 121 tests pass, 0 warnings
  • Streaming request body through Incoming -> Body conversion (frame-by-frame)
  • Streaming response body via wrap_stream through hyper boundary
  • Error propagation: streaming response body error reaches client
  • Error propagation: malformed chunked request doesn't crash server
  • Graceful shutdown with in-flight connections still completes requests
  • No flaky sleeps — TCP backlog + barriers for synchronization

@github-actions
Copy link
Copy Markdown

A new generated diff is ready to view.

  • No codegen difference in the AWS SDK
  • No codegen difference in the Client Test
  • No codegen difference in the Server Test
  • No codegen difference in the Server Test Python
  • No codegen difference in the Server Test Typescript

A new doc preview is ready to view.

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