-
Notifications
You must be signed in to change notification settings - Fork 225
docs(example)/custom future with multiple bodies #711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| [package] | ||
| name = "custom-future-multiple-bodies" | ||
| 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" | ||
| hyper = "1.10.1" | ||
| pin-project-lite = "0.2.17" | ||
| tokio = { version = "1.32.0", features = ["full"] } | ||
| tower = { version = "0.5", features = ["full"] } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # Custom future with multiple bodies | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we mention that the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dont quite understand what this means, doesnt the body stay fully preserved including the error? |
||
|
|
||
| This example serves to demonstrate how to build a service that returns a | ||
| response in case of an event (in this instance, a missing HTTP header) while | ||
| leaving the inner service's response untouched. | ||
|
|
||
| This requires wrapping the response body if the user wishes to leave the inner | ||
| service's body untouched. | ||
|
|
||
| ## Running the example | ||
|
|
||
| ``` | ||
| cargo run -p custom-future-multiple-bodies | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| use bytes::Bytes; | ||
| use http::{Request, Response, StatusCode}; | ||
| use http_body_util::{Either, Full}; | ||
| use pin_project_lite::pin_project; | ||
| use std::{ | ||
| future::Future, | ||
| pin::Pin, | ||
| task::{ready, Context, Poll}, | ||
| }; | ||
| use tower::Service; | ||
|
|
||
| use http_body_util::BodyExt; | ||
| use std::error::Error; | ||
| use tower::ServiceBuilder; | ||
| use tower::ServiceExt; | ||
|
|
||
| // think of Either as an enum that implements Body if both arms implement Body | ||
| // this allows us to combine multple bodies | ||
| type ResponseBody<B> = Either<B, Full<Bytes>>; | ||
|
|
||
| // some helper to go along with it | ||
| fn map_resp<B>(resp: Response<B>) -> Response<ResponseBody<B>> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this really carries its weight as a helper |
||
| resp.map(|body| Either::Left(body)) | ||
| } | ||
|
|
||
| fn new_err_resp<B>(status: StatusCode, body: &'static str) -> Response<ResponseBody<B>> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe call it |
||
| Response::builder() | ||
| .status(status) | ||
| .body(Either::Right(Full::from(body))) | ||
| .unwrap() | ||
| } | ||
|
|
||
| #[derive(Clone)] | ||
| pub struct RequireHeader<S> { | ||
| inner: S, | ||
| header_name: &'static str, | ||
| } | ||
|
|
||
| impl<S> RequireHeader<S> { | ||
| pub fn new(inner: S, header_name: &'static str) -> Self { | ||
| Self { inner, header_name } | ||
| } | ||
| } | ||
|
|
||
| impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for RequireHeader<S> | ||
| where | ||
| S: Service<Request<ReqBody>, Response = Response<ResBody>>, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would probably add |
||
| { | ||
| type Response = Response<ResponseBody<ResBody>>; | ||
| type Error = S::Error; | ||
| type Future = RequireHeaderFuture<S::Future>; | ||
|
|
||
| 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 !req.headers().contains_key(self.header_name) { | ||
| return RequireHeaderFuture::HeaderNotFound; | ||
| } | ||
| RequireHeaderFuture::Ok { | ||
| fut: self.inner.call(req), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pin_project! { | ||
| #[project = EnumProj] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
| pub enum RequireHeaderFuture<F> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be worth a doc comment pointing to how eg
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| Ok{ #[pin] fut: F }, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
| HeaderNotFound, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
| } | ||
| } | ||
|
|
||
| impl<F, E, ResBody> Future for RequireHeaderFuture<F> | ||
| where | ||
| F: Future<Output = Result<Response<ResBody>, E>>, | ||
| { | ||
| type Output = Result<Response<ResponseBody<ResBody>>, E>; | ||
|
|
||
| fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { | ||
| match self.project() { | ||
| EnumProj::Ok { fut } => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More readable as: |
||
| let res = ready!(fut.poll(cx)); | ||
|
|
||
| // we use our helper to unify the response body types | ||
| // its also possible to return a custom error response here if the inner service failed | ||
| Poll::Ready(res.map(|resp| map_resp(resp))) | ||
| } | ||
| EnumProj::HeaderNotFound => { | ||
| Poll::Ready(Ok(new_err_resp(StatusCode::BAD_REQUEST, "missing header"))) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[tokio::main] | ||
| async fn main() -> Result<(), Box<dyn Error>> { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need the unwrap here since we don't have I'd suggest instead widening the signature to |
||
| 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?; | ||
| assert_eq!(res_bad.status(), StatusCode::BAD_REQUEST); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| let body = res_bad.into_body().collect().await.unwrap(); | ||
| assert_eq!(body.to_bytes(), "missing header"); | ||
|
|
||
| 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?; | ||
| assert_eq!(res_good.status(), StatusCode::OK); | ||
|
|
||
| Ok(()) | ||
| } | ||
There was a problem hiding this comment.
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