diff --git a/lambda-http/Cargo.toml b/lambda-http/Cargo.toml index 11783a7f..c95bdddd 100644 --- a/lambda-http/Cargo.toml +++ b/lambda-http/Cargo.toml @@ -22,6 +22,7 @@ base64 = "0.13.0" bytes = "1" http = "0.2" http-body = "0.4" +hyper = "0.14" lambda_runtime = { path = "../lambda-runtime", version = "0.5" } serde = { version = "^1", features = ["derive"] } serde_json = "^1" @@ -32,4 +33,4 @@ query_map = { version = "0.4", features = ["url-query"] } log = "^0.4" maplit = "1.0" tokio = { version = "1.0", features = ["macros"] } -tower-http = { version = "0.2", features = ["cors"] } \ No newline at end of file +tower-http = { version = "0.2", features = ["cors", "trace"] } \ No newline at end of file diff --git a/lambda-http/examples/hello-cors.rs b/lambda-http/examples/hello-cors.rs index 275873a4..d24b7f71 100644 --- a/lambda-http/examples/hello-cors.rs +++ b/lambda-http/examples/hello-cors.rs @@ -20,7 +20,7 @@ async fn main() -> Result<(), Error> { async fn func(event: Request) -> Result, Error> { Ok(match event.query_string_parameters().first("first_name") { - Some(first_name) => format!("Hello, {}!", first_name).into_response(), + Some(first_name) => format!("Hello, {}!", first_name).into_response().await, _ => Response::builder() .status(400) .body("Empty first name".into()) diff --git a/lambda-http/examples/hello-http.rs b/lambda-http/examples/hello-http.rs index 5b679196..da3433c9 100644 --- a/lambda-http/examples/hello-http.rs +++ b/lambda-http/examples/hello-http.rs @@ -8,7 +8,7 @@ async fn main() -> Result<(), Error> { async fn func(event: Request) -> Result { Ok(match event.query_string_parameters().first("first_name") { - Some(first_name) => format!("Hello, {}!", first_name).into_response(), + Some(first_name) => format!("Hello, {}!", first_name).into_response().await, _ => Response::builder() .status(400) .body("Empty first name".into()) diff --git a/lambda-http/examples/hello-raw-http-path.rs b/lambda-http/examples/hello-raw-http-path.rs index 06bcdf71..ad1ea8aa 100644 --- a/lambda-http/examples/hello-raw-http-path.rs +++ b/lambda-http/examples/hello-raw-http-path.rs @@ -7,7 +7,9 @@ async fn main() -> Result<(), Error> { } async fn func(event: Request) -> Result { - let res = format!("The raw path for this request is: {}", event.raw_http_path()).into_response(); + let res = format!("The raw path for this request is: {}", event.raw_http_path()) + .into_response() + .await; Ok(res) } diff --git a/lambda-http/examples/hello-trace.rs b/lambda-http/examples/hello-trace.rs new file mode 100644 index 00000000..4712b567 --- /dev/null +++ b/lambda-http/examples/hello-trace.rs @@ -0,0 +1,15 @@ +use lambda_http::{tower::ServiceBuilder, Body, Error, IntoResponse, Request, Response}; +use tower_http::trace::TraceLayer; + +#[tokio::main] +async fn main() -> Result<(), Error> { + let service = ServiceBuilder::new() + .layer(TraceLayer::new_for_http()) + .service_fn(handler); + lambda_http::run(service).await?; + Ok(()) +} + +async fn handler(_event: Request) -> Result, Error> { + Ok("Success".into_response().await) +} diff --git a/lambda-http/examples/hello-tuple.rs b/lambda-http/examples/hello-tuple.rs new file mode 100644 index 00000000..90376526 --- /dev/null +++ b/lambda-http/examples/hello-tuple.rs @@ -0,0 +1,11 @@ +use lambda_http::{service_fn, Error, IntoResponse, Request}; + +#[tokio::main] +async fn main() -> Result<(), Error> { + lambda_http::run(service_fn(func)).await?; + Ok(()) +} + +async fn func(_event: Request) -> Result { + Ok((200, "Hello, world!")) +} diff --git a/lambda-http/examples/shared-resources-example.rs b/lambda-http/examples/shared-resources-example.rs index a90dd815..cf738a75 100644 --- a/lambda-http/examples/shared-resources-example.rs +++ b/lambda-http/examples/shared-resources-example.rs @@ -21,9 +21,12 @@ async fn main() -> Result<(), Error> { // Define a closure here that makes use of the shared client. let handler_func_closure = move |event: Request| async move { Ok(match event.query_string_parameters().first("first_name") { - Some(first_name) => shared_client_ref - .response(event.lambda_context().request_id, first_name) - .into_response(), + Some(first_name) => { + shared_client_ref + .response(event.lambda_context().request_id, first_name) + .into_response() + .await + } _ => Response::builder() .status(400) .body("Empty first name".into()) diff --git a/lambda-http/src/lib.rs b/lambda-http/src/lib.rs index 53fb9735..807ff916 100644 --- a/lambda-http/src/lib.rs +++ b/lambda-http/src/lib.rs @@ -65,6 +65,7 @@ extern crate maplit; pub use http::{self, Response}; use lambda_runtime::LambdaEvent; pub use lambda_runtime::{self, service_fn, tower, Context, Error, Service}; +use response::ResponseFuture; pub mod ext; pub mod request; @@ -91,7 +92,8 @@ pub type Request = http::Request; #[doc(hidden)] pub struct TransformResponse<'a, R, E> { request_origin: RequestOrigin, - fut: Pin> + 'a>>, + fut_req: Pin> + 'a>>, + fut_res: Option, } impl<'a, R, E> Future for TransformResponse<'a, R, E> @@ -101,11 +103,20 @@ where type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut TaskContext) -> Poll { - match self.fut.as_mut().poll(cx) { - Poll::Ready(result) => Poll::Ready( - result.map(|resp| LambdaResponse::from_response(&self.request_origin, resp.into_response())), - ), - Poll::Pending => Poll::Pending, + if let Some(fut_res) = self.fut_res.as_mut() { + match fut_res.as_mut().poll(cx) { + Poll::Ready(resp) => Poll::Ready(Ok(LambdaResponse::from_response(&self.request_origin, resp))), + Poll::Pending => Poll::Pending, + } + } else { + match self.fut_req.as_mut().poll(cx) { + Poll::Ready(Ok(resp)) => { + self.fut_res = Some(resp.into_response()); + Poll::Pending + } + Poll::Ready(Err(err)) => Poll::Ready(Err(err)), + Poll::Pending => Poll::Pending, + } } } } @@ -151,7 +162,11 @@ where let request_origin = req.payload.request_origin(); let event: Request = req.payload.into(); let fut = Box::pin(self.service.call(event.with_lambda_context(req.context))); - TransformResponse { request_origin, fut } + TransformResponse { + request_origin, + fut_req: fut, + fut_res: None, + } } } diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index 4ea9c895..38f0f8bd 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -4,11 +4,22 @@ use crate::request::RequestOrigin; use aws_lambda_events::encodings::Body; use aws_lambda_events::event::alb::AlbTargetGroupResponse; use aws_lambda_events::event::apigw::{ApiGatewayProxyResponse, ApiGatewayV2httpResponse}; +use http::StatusCode; use http::{ header::{CONTENT_TYPE, SET_COOKIE}, Response, }; +use http_body::Body as HttpBody; +use hyper::body::to_bytes; use serde::Serialize; +use std::convert::TryInto; +use std::future::ready; +use std::{ + any::{Any, TypeId}, + fmt, + future::Future, + pin::Pin, +}; /// Representation of Lambda response #[doc(hidden)] @@ -20,14 +31,11 @@ pub enum LambdaResponse { Alb(AlbTargetGroupResponse), } -/// tranformation from http type to internal type +/// Tranformation from http type to internal type impl LambdaResponse { - pub(crate) fn from_response(request_origin: &RequestOrigin, value: Response) -> Self - where - T: Into, - { + pub(crate) fn from_response(request_origin: &RequestOrigin, value: Response) -> Self { let (parts, bod) = value.into_parts(); - let (is_base64_encoded, body) = match bod.into() { + let (is_base64_encoded, body) = match bod { Body::Empty => (false, None), b @ Body::Text(_) => (false, Some(b)), b @ Body::Binary(_) => (true, Some(b)), @@ -87,71 +95,103 @@ impl LambdaResponse { } } -/// A conversion of self into a `Response` for various types. -/// -/// Implementations for `Response where B: Into`, -/// `B where B: Into` and `serde_json::Value` are provided -/// by default. -/// -/// # Example -/// -/// ```rust -/// use lambda_http::{Body, IntoResponse, Response}; +/// Trait for generating responses /// -/// assert_eq!( -/// "hello".into_response().body(), -/// Response::new(Body::from("hello")).body() -/// ); -/// ``` +/// Types that implement this trait can be used as return types for handler functions. pub trait IntoResponse { - /// Return a translation of `self` into a `Response` - fn into_response(self) -> Response; + /// Transform into a Response Future + fn into_response(self) -> ResponseFuture; } impl IntoResponse for Response where - B: Into, + B: IntoBody + 'static, { - fn into_response(self) -> Response { + fn into_response(self) -> ResponseFuture { let (parts, body) = self.into_parts(); - Response::from_parts(parts, body.into()) + + let fut = async { Response::from_parts(parts, body.into_body().await) }; + + Box::pin(fut) } } impl IntoResponse for String { - fn into_response(self) -> Response { - Response::new(Body::from(self)) + fn into_response(self) -> ResponseFuture { + Box::pin(ready(Response::new(Body::from(self)))) } } impl IntoResponse for &str { - fn into_response(self) -> Response { - Response::new(Body::from(self)) + fn into_response(self) -> ResponseFuture { + Box::pin(ready(Response::new(Body::from(self)))) } } impl IntoResponse for serde_json::Value { - fn into_response(self) -> Response { - Response::builder() - .header(CONTENT_TYPE, "application/json") - .body( - serde_json::to_string(&self) - .expect("unable to serialize serde_json::Value") - .into(), - ) - .expect("unable to build http::Response") + fn into_response(self) -> ResponseFuture { + Box::pin(async move { + Response::builder() + .header(CONTENT_TYPE, "application/json") + .body( + serde_json::to_string(&self) + .expect("unable to serialize serde_json::Value") + .into(), + ) + .expect("unable to build http::Response") + }) + } +} + +impl IntoResponse for (S, B) +where + S: TryInto + 'static, + S::Error: fmt::Debug, + B: Into + 'static, +{ + fn into_response(self) -> ResponseFuture { + Box::pin(async move { + Response::builder() + .status(self.0.try_into().expect("unable to transform status code")) + .body(self.1.into()) + .expect("unable to build http::Response") + }) } } +pub type ResponseFuture = Pin>>>; + +pub trait IntoBody { + fn into_body(self) -> BodyFuture; +} + +impl IntoBody for B +where + B: HttpBody + Unpin + 'static, + B::Error: fmt::Debug, +{ + fn into_body(self) -> BodyFuture { + if TypeId::of::() == self.type_id() { + let any_self = Box::new(self) as Box; + // Can safely unwrap here as we do type validation in the 'if' statement + Box::pin(ready(*any_self.downcast::().unwrap())) + } else { + Box::pin(async move { Body::from(to_bytes(self).await.expect("unable to read bytes from body").to_vec()) }) + } + } +} + +pub type BodyFuture = Pin>>; + #[cfg(test)] mod tests { use super::{Body, IntoResponse, LambdaResponse, RequestOrigin}; use http::{header::CONTENT_TYPE, Response}; use serde_json::{self, json}; - #[test] - fn json_into_response() { - let response = json!({ "hello": "lambda"}).into_response(); + #[tokio::test] + async fn json_into_response() { + let response = json!({ "hello": "lambda"}).into_response().await; match response.body() { Body::Text(json) => assert_eq!(json, r#"{"hello":"lambda"}"#), _ => panic!("invalid body"), @@ -165,9 +205,9 @@ mod tests { ) } - #[test] - fn text_into_response() { - let response = "text".into_response(); + #[tokio::test] + async fn text_into_response() { + let response = "text".into_response().await; match response.body() { Body::Text(text) => assert_eq!(text, "text"), _ => panic!("invalid body"), diff --git a/lambda-integration-tests/src/bin/http-fn.rs b/lambda-integration-tests/src/bin/http-fn.rs index b411b77f..4170d29f 100644 --- a/lambda-integration-tests/src/bin/http-fn.rs +++ b/lambda-integration-tests/src/bin/http-fn.rs @@ -1,11 +1,14 @@ -use lambda_http::{service_fn, Error, IntoResponse, Request, RequestExt, Response}; +use lambda_http::{service_fn, Body, Error, IntoResponse, Request, RequestExt, Response}; use tracing::info; async fn handler(event: Request) -> Result { let _context = event.lambda_context(); info!("[http-fn] Received event {} {}", event.method(), event.uri().path()); - Ok(Response::builder().status(200).body("Hello, world!").unwrap()) + Ok(Response::builder() + .status(200) + .body(Body::from("Hello, world!")) + .unwrap()) } #[tokio::main] diff --git a/lambda-integration-tests/src/bin/http-trait.rs b/lambda-integration-tests/src/bin/http-trait.rs index 091aec8e..67cc9fc5 100644 --- a/lambda-integration-tests/src/bin/http-trait.rs +++ b/lambda-integration-tests/src/bin/http-trait.rs @@ -1,4 +1,4 @@ -use lambda_http::{Error, Request, RequestExt, Response, Service}; +use lambda_http::{Body, Error, Request, RequestExt, Response, Service}; use std::{ future::{ready, Future}, pin::Pin, @@ -13,7 +13,7 @@ struct MyHandler { impl Service for MyHandler { type Error = Error; type Future = Pin> + Send>>; - type Response = Response<&'static str>; + type Response = Response; fn poll_ready(&mut self, _cx: &mut core::task::Context<'_>) -> core::task::Poll> { core::task::Poll::Ready(Ok(())) @@ -25,7 +25,7 @@ impl Service for MyHandler { info!("[http-trait] Lambda context: {:?}", request.lambda_context()); Box::pin(ready(Ok(Response::builder() .status(200) - .body("Hello, World!") + .body(Body::from("Hello, World!")) .unwrap()))) } }