Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@

This folder contains various examples of how to use tower-http.

- `axum-key-value-store`: A key/value store with an HTTP API built with axum.
- `warp-key-value-store`: A key/value store with an HTTP API built with warp.
- `tonic-key-value-store`: A key/value store with a gRPC API and client built with tonic.
- `custom-middleware-either`: A custom middleware showing how to unify body types using `Either`.
14 changes: 14 additions & 0 deletions examples/custom-middleware-either/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "custom-middleware-either"
version = "0.1.0"
authors = ["Tower Maintainers <team@tower-rs.com>"]
edition = "2021"
publish = false
license = "MIT"

[dependencies]
bytes = "1.12.0"
http = "1.4.2"
http-body-util = "0.1.3"
tokio = { version = "1.32.0", features = ["full"] }
tower = { version = "0.5", features = ["full"] }
17 changes: 17 additions & 0 deletions examples/custom-middleware-either/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# custom-middleware-either

Because a Tower `Service` requires a single, concrete `Response` type, you run into a type mismatch if your middleware returns an immediate error body in one branch, but defers to the inner service's body in the other.

This example solves that problem by using `http_body_util::Either<B, Full<Bytes>>` to gracefully unify the happy-path body and the early-rejection body, all without requiring dynamic allocations (boxing).

## How it works

- **Missing Header:** If the `x-api-key` header is missing, the middleware short-circuits the future using `std::future::ready` and immediately bounces the request with a `400 Bad Request` and a custom error body (`Either::Right`).
- **Valid Request:** If the header is present, the request is passed through to the inner service, which returns a `200 OK` (`Either::Left`).

## Running the example

```
RUST_LOG=custom_middleware_either=trace,tower_http=trace \
cargo run --bin custom-middleware-either
```
91 changes: 91 additions & 0 deletions examples/custom-middleware-either/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use bytes::Bytes;
use http::{Request, Response, StatusCode};
use http_body_util::{Either, Full};
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tower::{Service, ServiceBuilder, ServiceExt};

// We use `Either` here because this middleware might return a completely different
// body type (our error message) than the inner service returns. Both paths must
// resolve to a single concrete response type.
type ResponseBody<B> = Either<B, Full<Bytes>>;

#[derive(Clone)]
pub struct RequireHeader<S> {
inner: S,
header_name: &'static str,
}

impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for RequireHeader<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
S::Future: Send,
S::Error: Send,
ReqBody: Send + 'static,
ResBody: Send + 'static,
{
type Response = Response<ResponseBody<ResBody>>;
type Error = S::Error;
// We have to Box the Future dynamically here. Because we have two totally
// different return paths (the immediate error vs. waiting on the inner service)
// and Rust needs them to resolve to a single concrete type.
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
// If they don't have the header, bounce the request immediately
if !req.headers().contains_key(self.header_name) {
let body = Full::from("Missing required header");
let res = Response::builder()
.status(StatusCode::BAD_REQUEST)
// Either::Right explicitly tags our middleware-generated error body
.body(Either::Right(body))
.unwrap();

return Box::pin(std::future::ready(Ok(res)));
}

let mut inner = self.inner.clone();
Box::pin(async move {
let res = inner.call(req).await?;
// Either::Left maps the inner service's successful body into our unified type
Ok(res.map(Either::Left))
})
}
}

impl<S> RequireHeader<S> {
pub fn new(inner: S, header_name: &'static str) -> Self {
Self { inner, header_name }
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let inner_service = tower::service_fn(|_req: Request<Full<Bytes>>| async {
Ok::<_, std::convert::Infallible>(Response::new(Full::new(Bytes::from("Hello, World!"))))
});

let mut service = ServiceBuilder::new()
.layer_fn(|inner| RequireHeader::new(inner, "x-api-key"))
.service(inner_service);

let req_bad = Request::builder().body(Full::<Bytes>::default()).unwrap();
let res_bad = service.ready().await?.call(req_bad).await?;
println!("Bad: {}", res_bad.status());

let req_good = Request::builder()
.header("x-api-key", "secret")
.body(Full::<Bytes>::default())
.unwrap();
let res_good = service.ready().await?.call(req_good).await?;
println!("Good: {}", res_good.status());

Ok(())
}
Loading