Skip to content

docs(example)/custom future with multiple bodies - #711

Open
Reza-Darius wants to merge 3 commits into
tower-rs:mainfrom
Reza-Darius:docs(example)/custom-future-multiple-bodies
Open

docs(example)/custom future with multiple bodies#711
Reza-Darius wants to merge 3 commits into
tower-rs:mainfrom
Reza-Darius:docs(example)/custom-future-multiple-bodies

Conversation

@Reza-Darius

Copy link
Copy Markdown

This PR builds on this PR, which explores idomatic solutions to dealing with multiple HTTP bodies in tower services

Motivation

@Isvane did a great job with his proposal, so i wanted to iterate on it by bringing it closer to how i solved this problem in my own project. Primarily, by introducing a custom future.

Changes

  • removed BoxBody alias
  • removed some service trait bounds
  • added helper functions
  • replaced Pin Box Future with custom future.
  • added asserts to main()

Discussion

While i think this is a decently elegant solution, it could be argued its not idiomatic if you compare how this problem is solved in tower-http/limit.

A potential foot gun im wondering about is the trait bound on Either which states that both arms need to have the same underlying Data type. Right now the example leaves the response body of the inner service entirely generic, but this could lead to a nasty compile error down the road should the Data types mismatch.

tower-http/limit imposes a Body<Data = Bytes> bound on its Body implementation for it's wrapper type and im wondering whether this example should include it too, and if so, where.

@Isvane

Isvane commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Thanks so much for iterating on this! ngl im still learning so custom futures and tower trait bounds are still kinda wizardry to me lol. your version looks way cleaner and more idiomatic.

Should i just close my pr so we can use yours?

bytes = "1.12.0"
http = "1.4.2"
http-body-util = "0.1.3"
hyper = "1.10.1"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hyper is not needed here


let req_bad = Request::builder().body(Full::<Bytes>::default()).unwrap();
let res_bad = service.ready().await?.call(req_bad).await?;
assert_eq!(res_bad.status(), StatusCode::BAD_REQUEST);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably these assertions should be in test modules? That matches the other examples.

I'd also probably add some println!s so that cargo run -p custom-future-multiple-bodies prints something, given the README guides the user to run it.


impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for RequireHeader<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably add ResBody: http_body::Body<Data = Bytes> here, yeah, at the cost of taking the http-body = "1" dependency. That moves any error out to the middleware layer, which is clearer.

@@ -0,0 +1,14 @@
# Custom future with multiple bodies

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we mention that the Either is convenient but loses the concrete error type, and that you can refer to the tower_http::limit wrapper to preserve the type?

pin_project! {
#[project = EnumProj]
pub enum RequireHeaderFuture<F> {
Ok{ #[pin] fut: F },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Future to match the repo conventions?


pin_project! {
#[project = EnumProj]
pub enum RequireHeaderFuture<F> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be worth a doc comment pointing to how eg limit/future.rs encapsulate these types for semver safety, if used in a library? This seems fine for application usage.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't particularly mind demonstrating the encapsulated pattern in general, it's not particularly more verbose, up to you.


fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.project() {
EnumProj::Ok { fut } => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More readable as:

  fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
      let res = match self.project() {
              // `?` propagates an inner-service error. to turn it into a response
              // instead, replace `?` with
              //     .unwrap_or_else(|_| rejection(StatusCode::BAD_GATEWAY, "..."))
              // to convert every error, or `.or_else(..)` to convert only some and
              // keep propagating the rest.
          ResFutProj::Future { future } => ready!(future.poll(cx))?.map(Either::Left),
          ResFutProj::MissingHeader => rejection(StatusCode::BAD_REQUEST, "missing header"),
      };

      Poll::Ready(Ok(res))
  

}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need the unwrap here since we don't have Box<dyn Error>: From<Box<dyn Error + Send + Sync>>.

I'd suggest instead widening the signature to Result<(), Box<dyn Error + Send + Sync>> (or just Result<(), tower::BoxError>, which is equivalent) to avoid both the unwrap and the need to coerce explicitly to work around the missing From.

resp.map(|body| Either::Left(body))
}

fn new_err_resp<B>(status: StatusCode, body: &'static str) -> Response<ResponseBody<B>> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe call it rejection?

type ResponseBody<B> = Either<B, Full<Bytes>>;

// some helper to go along with it
fn map_resp<B>(resp: Response<B>) -> Response<ResponseBody<B>> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this really carries its weight as a helper

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.

3 participants