From e68561b8f9a2e6e1901c055a5561f2e918af18cf Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Thu, 14 Apr 2022 15:17:08 +0200 Subject: [PATCH 01/10] feat!(lambda-http): accept http_body::Body in responses --- lambda-http/Cargo.toml | 1 + lambda-http/src/lib.rs | 27 ++++++++--- lambda-http/src/response.rs | 91 +++++++++++++++++++++++-------------- 3 files changed, 79 insertions(+), 40 deletions(-) diff --git a/lambda-http/Cargo.toml b/lambda-http/Cargo.toml index a032a131..2d1a4a49 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" diff --git a/lambda-http/src/lib.rs b/lambda-http/src/lib.rs index 62de0f13..31179c00 100644 --- a/lambda-http/src/lib.rs +++ b/lambda-http/src/lib.rs @@ -67,6 +67,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; @@ -93,7 +94,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> @@ -103,11 +105,22 @@ 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, + } } } } @@ -153,7 +166,7 @@ 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..1b8c6d48 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -8,7 +8,15 @@ use http::{ header::{CONTENT_TYPE, SET_COOKIE}, Response, }; +use http_body::Body as HttpBody; +use hyper::body::to_bytes; use serde::Serialize; +use std::future::ready; +use std::{ + any::{Any, TypeId}, + pin::Pin, + future::Future, +}; /// Representation of Lambda response #[doc(hidden)] @@ -87,52 +95,41 @@ 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}; -/// -/// assert_eq!( -/// "hello".into_response().body(), -/// Response::new(Body::from("hello")).body() -/// ); -/// ``` pub trait IntoResponse { - /// Return a translation of `self` into a `Response` - fn into_response(self) -> Response; + 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() + fn into_response(self) -> ResponseFuture { + Box::pin(async move { + Response::builder() .header(CONTENT_TYPE, "application/json") .body( serde_json::to_string(&self) @@ -140,18 +137,46 @@ impl IntoResponse for serde_json::Value { .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: std::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 +190,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"), From 418ee4cfd901378a9a3ab93faede2701208e3f83 Mon Sep 17 00:00:00 2001 From: Nicolas Moutschen Date: Tue, 19 Apr 2022 09:05:30 +0200 Subject: [PATCH 02/10] fix(lambda-http): fix examples and docs --- examples/http-cors/src/main.rs | 2 +- examples/http-shared-resource/src/main.rs | 9 ++-- lambda-http/src/lib.rs | 12 ++--- lambda-http/src/response.rs | 45 +++++++++---------- lambda-integration-tests/src/bin/http-fn.rs | 7 ++- .../src/bin/http-trait.rs | 6 +-- 6 files changed, 43 insertions(+), 38 deletions(-) diff --git a/examples/http-cors/src/main.rs b/examples/http-cors/src/main.rs index 459dc6f3..7e1b21fa 100644 --- a/examples/http-cors/src/main.rs +++ b/examples/http-cors/src/main.rs @@ -29,7 +29,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/examples/http-shared-resource/src/main.rs b/examples/http-shared-resource/src/main.rs index 4491ac75..48bab471 100644 --- a/examples/http-shared-resource/src/main.rs +++ b/examples/http-shared-resource/src/main.rs @@ -29,9 +29,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 { Result::, Error>::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 31179c00..51eab8bf 100644 --- a/lambda-http/src/lib.rs +++ b/lambda-http/src/lib.rs @@ -107,9 +107,7 @@ where fn poll(mut self: Pin<&mut Self>, cx: &mut TaskContext) -> Poll { 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::Ready(resp) => Poll::Ready(Ok(LambdaResponse::from_response(&self.request_origin, resp))), Poll::Pending => Poll::Pending, } } else { @@ -117,7 +115,7 @@ where 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, } @@ -166,7 +164,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_req: fut, fut_res: None, } + TransformResponse { + request_origin, + fut_req: fut, + fut_res: None, + } } } diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index 1b8c6d48..2d9e92da 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -14,8 +14,9 @@ use serde::Serialize; use std::future::ready; use std::{ any::{Any, TypeId}, - pin::Pin, + fmt, future::Future, + pin::Pin, }; /// Representation of Lambda response @@ -28,14 +29,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)), @@ -95,7 +93,11 @@ impl LambdaResponse { } } +/// Trait for generating responses +/// +/// Types that implement this trait can be used as return types for handler functions. pub trait IntoResponse { + /// Transform into a Response Future fn into_response(self) -> ResponseFuture; } @@ -106,9 +108,7 @@ where fn into_response(self) -> ResponseFuture { let (parts, body) = self.into_parts(); - let fut = async { - Response::from_parts(parts, body.into_body().await) - }; + let fut = async { Response::from_parts(parts, body.into_body().await) }; Box::pin(fut) } @@ -130,19 +130,18 @@ impl IntoResponse for serde_json::Value { 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") + .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") }) } } -pub type ResponseFuture = Pin>>>; - +pub type ResponseFuture = Pin>>>; pub trait IntoBody { fn into_body(self) -> BodyFuture; @@ -151,7 +150,7 @@ pub trait IntoBody { impl IntoBody for B where B: HttpBody + Unpin + 'static, - B::Error: std::fmt::Debug, + B::Error: fmt::Debug, { fn into_body(self) -> BodyFuture { if TypeId::of::() == self.type_id() { @@ -159,14 +158,12 @@ where // 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()) - }) + Box::pin(async move { Body::from(to_bytes(self).await.expect("unable to read bytes from body").to_vec()) }) } } } -pub type BodyFuture = Pin>>; +pub type BodyFuture = Pin>>; #[cfg(test)] mod tests { 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()))) } } From 44edec48c739c7d25d71df8a900d7d3fbd3e9f8f Mon Sep 17 00:00:00 2001 From: Hugo Bastien Date: Wed, 15 Jun 2022 11:49:05 -0400 Subject: [PATCH 03/10] chore: try adapt makefile to m1 mac `cross` didn't work - zigbuild works as a replacement additonal change: match the layer name with the executable --- Makefile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index d1eb2c99..cb00545c 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,10 @@ INTEG_EXTENSIONS := extension-fn extension-trait logs-trait # Using musl to run extensions on both AL1 and AL2 INTEG_ARCH := x86_64-unknown-linux-musl +define uppercase +$(shell sed -r 's/(^|-)(\w)/\U\2/g' <<< $(1)) +endef + pr-check: cargo +1.54.0 check --all cargo +stable fmt --all -- --check @@ -15,7 +19,7 @@ pr-check: integration-tests: # Build Integration functions - cross build --release --target $(INTEG_ARCH) -p lambda_integration_tests + cargo zigbuild --release --target $(INTEG_ARCH) -p lambda_integration_tests rm -rf ./build mkdir -p ./build ${MAKE} ${MAKEOPTS} $(foreach function,${INTEG_FUNCTIONS_BUILD}, build-integration-function-${function}) @@ -37,7 +41,7 @@ build-integration-function-%: build-integration-extension-%: mkdir -p ./build/$*/extensions - cp -v ./target/$(INTEG_ARCH)/release/$* ./build/$*/extensions/$* + cp -v ./target/$(INTEG_ARCH)/release/$* ./build/$*/extensions/$(call uppercase,$*) invoke-integration-function-%: aws lambda invoke --function-name $$(aws cloudformation describe-stacks --stack-name $(INTEG_STACK_NAME) \ @@ -56,4 +60,3 @@ invoke-integration-api-%: curl -X POST -d '{"command": "hello"}' $(API_URL)/trait/post curl -X POST -d '{"command": "hello"}' $(API_URL)/al2/post curl -X POST -d '{"command": "hello"}' $(API_URL)/al2-trait/post - \ No newline at end of file From b56e75906632fdcd694e03e150a1414d187997b1 Mon Sep 17 00:00:00 2001 From: Hugo Bastien Date: Mon, 20 Jun 2022 10:43:46 -0400 Subject: [PATCH 04/10] fix: notify waker to process resp future --- lambda-http/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lambda-http/src/lib.rs b/lambda-http/src/lib.rs index 51eab8bf..8ed42ca6 100644 --- a/lambda-http/src/lib.rs +++ b/lambda-http/src/lib.rs @@ -114,6 +114,7 @@ where match self.fut_req.as_mut().poll(cx) { Poll::Ready(Ok(resp)) => { self.fut_res = Some(resp.into_response()); + cx.waker().wake_by_ref(); Poll::Pending } Poll::Ready(Err(err)) => Poll::Ready(Err(err)), From d58a1bc68b810b924d7c38a4c2c3da4849508897 Mon Sep 17 00:00:00 2001 From: Hugo Bastien Date: Mon, 20 Jun 2022 13:13:38 -0400 Subject: [PATCH 05/10] refactor: turn struct into enum --- lambda-http/src/lib.rs | 35 +++++++++++++++-------------------- lambda-http/src/request.rs | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/lambda-http/src/lib.rs b/lambda-http/src/lib.rs index 8ed42ca6..dd02ab4d 100644 --- a/lambda-http/src/lib.rs +++ b/lambda-http/src/lib.rs @@ -67,6 +67,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 request::RequestFuture; use response::ResponseFuture; pub mod ext; @@ -92,10 +93,9 @@ pub type Request = http::Request; /// /// This is used by the `Adapter` wrapper and is completely internal to the `lambda_http::run` function. #[doc(hidden)] -pub struct TransformResponse<'a, R, E> { - request_origin: RequestOrigin, - fut_req: Pin> + 'a>>, - fut_res: Option, +pub enum TransformResponse<'a, R, E> { + Request(RequestOrigin, RequestFuture<'a, R, E>), + Response(RequestOrigin, ResponseFuture), } impl<'a, R, E> Future for TransformResponse<'a, R, E> @@ -105,21 +105,19 @@ where type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut TaskContext) -> Poll { - 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) { + match *self { + TransformResponse::Request(ref mut origin, ref mut request) => match request.as_mut().poll(cx) { Poll::Ready(Ok(resp)) => { - self.fut_res = Some(resp.into_response()); - cx.waker().wake_by_ref(); - Poll::Pending + *self = TransformResponse::Response(origin.clone(), resp.into_response()); + self.poll(cx) } Poll::Ready(Err(err)) => Poll::Ready(Err(err)), Poll::Pending => Poll::Pending, - } + }, + TransformResponse::Response(ref mut origin, ref mut response) => match response.as_mut().poll(cx) { + Poll::Ready(resp) => Poll::Ready(Ok(LambdaResponse::from_response(origin, resp))), + Poll::Pending => Poll::Pending, + }, } } } @@ -165,11 +163,8 @@ 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_req: fut, - fut_res: None, - } + + TransformResponse::Request(request_origin, fut) } } diff --git a/lambda-http/src/request.rs b/lambda-http/src/request.rs index d4770dd6..871453d0 100644 --- a/lambda-http/src/request.rs +++ b/lambda-http/src/request.rs @@ -14,6 +14,8 @@ use http::header::HeaderName; use query_map::QueryMap; use serde::Deserialize; use serde_json::error::Error as JsonError; +use std::future::Future; +use std::pin::Pin; use std::{io::Read, mem}; /// Internal representation of an Lambda http event from @@ -45,6 +47,9 @@ impl LambdaRequest { } } +/// RequestFuture type +pub type RequestFuture<'a, R, E> = Pin> + 'a>>; + /// Represents the origin from which the lambda was requested from. #[doc(hidden)] #[derive(Debug)] @@ -59,6 +64,17 @@ pub enum RequestOrigin { WebSocket, } +impl RequestOrigin { + pub(crate) fn clone(&self) -> RequestOrigin { + match self { + RequestOrigin::ApiGatewayV1 => RequestOrigin::ApiGatewayV1, + RequestOrigin::ApiGatewayV2 => RequestOrigin::ApiGatewayV2, + RequestOrigin::Alb => RequestOrigin::Alb, + RequestOrigin::WebSocket => RequestOrigin::WebSocket, + } + } +} + fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request { let http_method = ag.request_context.http.method.clone(); let raw_path = ag.raw_path.unwrap_or_default(); From e9111a71d6e63a07e41000ba4f28e6c82ecbbf2b Mon Sep 17 00:00:00 2001 From: Hugo Bastien Date: Tue, 21 Jun 2022 17:23:07 -0400 Subject: [PATCH 06/10] feat: impl into_response for bytes --- lambda-http/src/response.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index 2d9e92da..025610d0 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -126,6 +126,18 @@ impl IntoResponse for &str { } } +impl IntoResponse for &[u8] { + fn into_response(self) -> ResponseFuture { + Box::pin(ready(Response::new(Body::from(self)))) + } +} + +impl IntoResponse for Vec { + fn into_response(self) -> ResponseFuture { + Box::pin(ready(Response::new(Body::from(self)))) + } +} + impl IntoResponse for serde_json::Value { fn into_response(self) -> ResponseFuture { Box::pin(async move { @@ -196,6 +208,15 @@ mod tests { } } + #[tokio::test] + async fn bytes_into_response() { + let response = "text".as_bytes().into_response().await; + match response.body() { + Body::Binary(data) => assert_eq!(data, "text".as_bytes()), + _ => panic!("invalid body"), + } + } + #[test] fn serialize_multi_value_headers() { let res = LambdaResponse::from_response( From 9e053ef1d4ae892fc9ce314e1fbef74e6b2fcb47 Mon Sep 17 00:00:00 2001 From: Hugo Bastien Date: Tue, 21 Jun 2022 22:27:22 -0400 Subject: [PATCH 07/10] fix: mispelling Co-authored-by: David Calavera --- lambda-http/src/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index 025610d0..4e14ef1c 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -29,7 +29,7 @@ pub enum LambdaResponse { Alb(AlbTargetGroupResponse), } -/// Tranformation from http type to internal type +/// Transformation from http type to internal type impl LambdaResponse { pub(crate) fn from_response(request_origin: &RequestOrigin, value: Response) -> Self { let (parts, bod) = value.into_parts(); From 9cd8f3fdd8787628f43e32a4a63588747f7825a1 Mon Sep 17 00:00:00 2001 From: Hugo Bastien Date: Wed, 6 Jul 2022 17:09:39 -0400 Subject: [PATCH 08/10] feat: respond based on content headers --- lambda-http/src/response.rs | 125 ++++++++++++++++++++++++++++++------ 1 file changed, 107 insertions(+), 18 deletions(-) diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index 4e14ef1c..3883c4b9 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -4,6 +4,8 @@ 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::header::CONTENT_ENCODING; +use http::HeaderMap; use http::{ header::{CONTENT_TYPE, SET_COOKIE}, Response, @@ -12,12 +14,8 @@ use http_body::Body as HttpBody; use hyper::body::to_bytes; use serde::Serialize; use std::future::ready; -use std::{ - any::{Any, TypeId}, - fmt, - future::Future, - pin::Pin, -}; +use std::str::from_utf8; +use std::{fmt, future::Future, pin::Pin}; /// Representation of Lambda response #[doc(hidden)] @@ -103,12 +101,13 @@ pub trait IntoResponse { impl IntoResponse for Response where - B: IntoBody + 'static, + B: ConvertBody + 'static, { fn into_response(self) -> ResponseFuture { let (parts, body) = self.into_parts(); + let headers = parts.headers.clone(); - let fut = async { Response::from_parts(parts, body.into_body().await) }; + let fut = async { Response::from_parts(parts, body.convert(headers).await) }; Box::pin(fut) } @@ -155,32 +154,69 @@ impl IntoResponse for serde_json::Value { pub type ResponseFuture = Pin>>>; -pub trait IntoBody { - fn into_body(self) -> BodyFuture; +pub trait ConvertBody { + fn convert(self, parts: HeaderMap) -> BodyFuture; } -impl IntoBody for B +impl ConvertBody 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())) + fn convert(self, headers: HeaderMap) -> BodyFuture { + if headers.get(CONTENT_ENCODING).is_some() { + return convert_to_binary(self); + } + + let content_type = if let Some(value) = headers.get(http::header::CONTENT_TYPE) { + value.to_str().unwrap_or_default() } else { - Box::pin(async move { Body::from(to_bytes(self).await.expect("unable to read bytes from body").to_vec()) }) + // Content-Type and Content-Encoding not set, passthrough as utf8 text + return convert_to_text(self); + }; + + if content_type.starts_with("text") + || content_type.starts_with("application/json") + || content_type.starts_with("application/javascript") + || content_type.starts_with("application/xml") + { + return convert_to_text(self); } + + convert_to_binary(self) } } +fn convert_to_binary(body: B) -> BodyFuture +where + B: HttpBody + Unpin + 'static, + B::Error: fmt::Debug, +{ + Box::pin(async move { Body::from(to_bytes(body).await.expect("unable to read bytes from body").to_vec()) }) +} + +fn convert_to_text(body: B) -> BodyFuture +where + B: HttpBody + Unpin + 'static, + B::Error: fmt::Debug, +{ + // assumes utf-8 + Box::pin(async move { + let bytes = to_bytes(body).await.expect("unable to read bytes from body"); + Body::from(from_utf8(&bytes).expect("response body not utf-8")) + }) +} + pub type BodyFuture = Pin>>; #[cfg(test)] mod tests { use super::{Body, IntoResponse, LambdaResponse, RequestOrigin}; - use http::{header::CONTENT_TYPE, Response}; + use http::{ + header::{CONTENT_ENCODING, CONTENT_TYPE}, + Response, + }; + use hyper::Body as HyperBody; use serde_json::{self, json}; #[tokio::test] @@ -217,6 +253,59 @@ mod tests { } } + #[tokio::test] + async fn content_encoding_header() { + // Drive the implementation by using `hyper::Body` instead of + // of `aws_lambda_events::encodings::Body` + let response = Response::builder() + .header(CONTENT_ENCODING, "gzip") + .body(HyperBody::from("000000".as_bytes())) + .expect("unable to build http::Response"); + let response = response.into_response().await; + let response = LambdaResponse::from_response(&RequestOrigin::ApiGatewayV2, response); + + let json = serde_json::to_string(&response).expect("failed to serialize to json"); + assert_eq!( + json, + r#"{"statusCode":200,"headers":{"content-encoding":"gzip"},"multiValueHeaders":{"content-encoding":["gzip"]},"body":"MDAwMDAw","isBase64Encoded":true,"cookies":[]}"# + ) + } + + #[tokio::test] + async fn content_type_header() { + // Drive the implementation by using `hyper::Body` instead of + // of `aws_lambda_events::encodings::Body` + let response = Response::builder() + .header(CONTENT_TYPE, "application/json") + .body(HyperBody::from("000000".as_bytes())) + .expect("unable to build http::Response"); + let response = response.into_response().await; + let response = LambdaResponse::from_response(&RequestOrigin::ApiGatewayV2, response); + + let json = serde_json::to_string(&response).expect("failed to serialize to json"); + assert_eq!( + json, + r#"{"statusCode":200,"headers":{"content-type":"application/json"},"multiValueHeaders":{"content-type":["application/json"]},"body":"000000","isBase64Encoded":false,"cookies":[]}"# + ) + } + + #[tokio::test] + async fn content_headers_unset() { + // Drive the implementation by using `hyper::Body` instead of + // of `aws_lambda_events::encodings::Body` + let response = Response::builder() + .body(HyperBody::from("000000".as_bytes())) + .expect("unable to build http::Response"); + let response = response.into_response().await; + let response = LambdaResponse::from_response(&RequestOrigin::ApiGatewayV2, response); + + let json = serde_json::to_string(&response).expect("failed to serialize to json"); + assert_eq!( + json, + r#"{"statusCode":200,"headers":{},"multiValueHeaders":{},"body":"000000","isBase64Encoded":false,"cookies":[]}"# + ) + } + #[test] fn serialize_multi_value_headers() { let res = LambdaResponse::from_response( From 2632aad27b92930a1267db6c03142a77ebc915f8 Mon Sep 17 00:00:00 2001 From: Hugo Bastien Date: Wed, 6 Jul 2022 21:34:31 -0400 Subject: [PATCH 09/10] feat: consider content type charset --- lambda-http/Cargo.toml | 2 ++ lambda-http/src/response.rs | 45 ++++++++++++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/lambda-http/Cargo.toml b/lambda-http/Cargo.toml index 40375f4c..92955045 100644 --- a/lambda-http/Cargo.toml +++ b/lambda-http/Cargo.toml @@ -30,6 +30,8 @@ serde = { version = "^1", features = ["derive"] } serde_json = "^1" serde_urlencoded = "0.7.0" query_map = { version = "0.5", features = ["url-query"] } +mime = "0.3.16" +encoding_rs = "0.8.31" [dependencies.aws_lambda_events] version = "^0.6.3" diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index 30832c13..6ab458cc 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -8,6 +8,7 @@ use aws_lambda_events::event::alb::AlbTargetGroupResponse; use aws_lambda_events::event::apigw::ApiGatewayProxyResponse; #[cfg(feature = "apigw_http")] use aws_lambda_events::event::apigw::ApiGatewayV2httpResponse; +use encoding_rs::Encoding; use http::header::CONTENT_ENCODING; use http::HeaderMap; use http::{ @@ -16,9 +17,10 @@ use http::{ }; use http_body::Body as HttpBody; use hyper::body::to_bytes; +use mime::{Mime, CHARSET}; use serde::Serialize; +use std::borrow::Cow; use std::future::ready; -use std::str::from_utf8; use std::{fmt, future::Future, pin::Pin}; /// Representation of Lambda response @@ -183,7 +185,7 @@ where value.to_str().unwrap_or_default() } else { // Content-Type and Content-Encoding not set, passthrough as utf8 text - return convert_to_text(self); + return convert_to_text(self, "utf-8".to_string()); }; if content_type.starts_with("text") @@ -191,7 +193,7 @@ where || content_type.starts_with("application/javascript") || content_type.starts_with("application/xml") { - return convert_to_text(self); + return convert_to_text(self, content_type.to_string()); } convert_to_binary(self) @@ -206,15 +208,30 @@ where Box::pin(async move { Body::from(to_bytes(body).await.expect("unable to read bytes from body").to_vec()) }) } -fn convert_to_text(body: B) -> BodyFuture +fn convert_to_text(body: B, content_type: String) -> BodyFuture where B: HttpBody + Unpin + 'static, B::Error: fmt::Debug, { + let mime_type = content_type.parse::(); + + let encoding = match mime_type.as_ref() { + Ok(mime) => mime.get_param(CHARSET).unwrap_or(mime::UTF_8), + Err(_) => mime::UTF_8, + }; + + let label = encoding.as_ref().as_bytes(); + let encoding = Encoding::for_label(label).unwrap_or(encoding_rs::UTF_8); + // assumes utf-8 Box::pin(async move { let bytes = to_bytes(body).await.expect("unable to read bytes from body"); - Body::from(from_utf8(&bytes).expect("response body not utf-8")) + let (content, _, _) = encoding.decode(&bytes); + + match content { + Cow::Borrowed(content) => Body::from(content), + Cow::Owned(content) => Body::from(content), + } }) } @@ -300,6 +317,24 @@ mod tests { ) } + #[tokio::test] + async fn charset_content_type_header() { + // Drive the implementation by using `hyper::Body` instead of + // of `aws_lambda_events::encodings::Body` + let response = Response::builder() + .header(CONTENT_TYPE, "application/json; charset=utf-16") + .body(HyperBody::from("000000".as_bytes())) + .expect("unable to build http::Response"); + let response = response.into_response().await; + let response = LambdaResponse::from_response(&RequestOrigin::ApiGatewayV2, response); + + let json = serde_json::to_string(&response).expect("failed to serialize to json"); + assert_eq!( + json, + r#"{"statusCode":200,"headers":{"content-type":"application/json; charset=utf-16"},"multiValueHeaders":{"content-type":["application/json; charset=utf-16"]},"body":"〰〰〰","isBase64Encoded":false,"cookies":[]}"# + ) + } + #[tokio::test] async fn content_headers_unset() { // Drive the implementation by using `hyper::Body` instead of From 7b00ea7c966ead5e0890068db1d07ccf823e0f9e Mon Sep 17 00:00:00 2001 From: Hugo Bastien Date: Wed, 6 Jul 2022 23:01:51 -0400 Subject: [PATCH 10/10] feat: adjust examples + code review comments --- examples/http-basic-lambda/src/main.rs | 6 +++--- examples/http-query-parameters/src/main.rs | 2 +- examples/http-raw-path/src/main.rs | 4 +++- lambda-http/src/request.rs | 13 +------------ lambda-http/src/response.rs | 6 +++--- 5 files changed, 11 insertions(+), 20 deletions(-) diff --git a/examples/http-basic-lambda/src/main.rs b/examples/http-basic-lambda/src/main.rs index cf15fec9..df15ae6c 100644 --- a/examples/http-basic-lambda/src/main.rs +++ b/examples/http-basic-lambda/src/main.rs @@ -1,10 +1,10 @@ -use lambda_http::{run, service_fn, Error, IntoResponse, Request, Response}; +use lambda_http::{run, service_fn, Body, Error, Request, Response}; /// This is the main body for the function. /// Write your code inside it. /// There are some code examples in the Runtime repository: /// - https://github.com/awslabs/aws-lambda-rust-runtime/tree/main/examples -async fn function_handler(_event: Request) -> Result { +async fn function_handler(_event: Request) -> Result, Error> { // Extract some useful information from the request // Return something that implements IntoResponse. @@ -12,7 +12,7 @@ async fn function_handler(_event: Request) -> Result { let resp = Response::builder() .status(200) .header("content-type", "text/html") - .body("Hello AWS Lambda HTTP request") + .body("Hello AWS Lambda HTTP request".into()) .map_err(Box::new)?; Ok(resp) } diff --git a/examples/http-query-parameters/src/main.rs b/examples/http-query-parameters/src/main.rs index 1e499948..03e4b939 100644 --- a/examples/http-query-parameters/src/main.rs +++ b/examples/http-query-parameters/src/main.rs @@ -7,7 +7,7 @@ use lambda_http::{run, service_fn, Error, IntoResponse, Request, RequestExt, Res async fn function_handler(event: Request) -> Result { // Extract some useful information from the request 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/examples/http-raw-path/src/main.rs b/examples/http-raw-path/src/main.rs index 1caafeab..f88b7b64 100644 --- a/examples/http-raw-path/src/main.rs +++ b/examples/http-raw-path/src/main.rs @@ -15,7 +15,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/src/request.rs b/lambda-http/src/request.rs index 740df5e1..77eb5913 100644 --- a/lambda-http/src/request.rs +++ b/lambda-http/src/request.rs @@ -63,7 +63,7 @@ pub type RequestFuture<'a, R, E> = Pin> + ' /// Represents the origin from which the lambda was requested from. #[doc(hidden)] -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum RequestOrigin { /// API Gateway request origin #[cfg(feature = "apigw_rest")] @@ -79,17 +79,6 @@ pub enum RequestOrigin { WebSocket, } -impl RequestOrigin { - pub(crate) fn clone(&self) -> RequestOrigin { - match self { - RequestOrigin::ApiGatewayV1 => RequestOrigin::ApiGatewayV1, - RequestOrigin::ApiGatewayV2 => RequestOrigin::ApiGatewayV2, - RequestOrigin::Alb => RequestOrigin::Alb, - RequestOrigin::WebSocket => RequestOrigin::WebSocket, - } - } -} - #[cfg(feature = "apigw_http")] fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request { let http_method = ag.request_context.http.method.clone(); diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index 6ab458cc..adfe3528 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -185,7 +185,7 @@ where value.to_str().unwrap_or_default() } else { // Content-Type and Content-Encoding not set, passthrough as utf8 text - return convert_to_text(self, "utf-8".to_string()); + return convert_to_text(self, "utf-8"); }; if content_type.starts_with("text") @@ -193,7 +193,7 @@ where || content_type.starts_with("application/javascript") || content_type.starts_with("application/xml") { - return convert_to_text(self, content_type.to_string()); + return convert_to_text(self, content_type); } convert_to_binary(self) @@ -208,7 +208,7 @@ where Box::pin(async move { Body::from(to_bytes(body).await.expect("unable to read bytes from body").to_vec()) }) } -fn convert_to_text(body: B, content_type: String) -> BodyFuture +fn convert_to_text(body: B, content_type: &str) -> BodyFuture where B: HttpBody + Unpin + 'static, B::Error: fmt::Debug,