-
Couldn't load subscription status.
- Fork 379
feat(lambda-http): accept http_body::Body in responses #491
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
Changes from 7 commits
e68561b
418ee4c
44edec4
b56e759
d58a1bc
e9111a7
9e053ef
7d37ace
9cd8f3f
cf049d4
2632aad
7b00ea7
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 | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,7 +8,16 @@ 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}, | ||||||||||||||||||||
| fmt, | ||||||||||||||||||||
| future::Future, | ||||||||||||||||||||
| pin::Pin, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Representation of Lambda response | ||||||||||||||||||||
| #[doc(hidden)] | ||||||||||||||||||||
|
|
@@ -20,14 +29,11 @@ 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<T>(request_origin: &RequestOrigin, value: Response<T>) -> Self | ||||||||||||||||||||
| where | ||||||||||||||||||||
| T: Into<Body>, | ||||||||||||||||||||
| { | ||||||||||||||||||||
| pub(crate) fn from_response(request_origin: &RequestOrigin, value: Response<Body>) -> 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 +93,99 @@ impl LambdaResponse { | |||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// A conversion of self into a `Response<Body>` for various types. | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// Implementations for `Response<B> where B: Into<Body>`, | ||||||||||||||||||||
| /// `B where B: Into<Body>` 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<Body>` | ||||||||||||||||||||
| fn into_response(self) -> Response<Body>; | ||||||||||||||||||||
| /// Transform into a Response<Body> Future | ||||||||||||||||||||
| fn into_response(self) -> ResponseFuture; | ||||||||||||||||||||
bnusunny marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl<B> IntoResponse for Response<B> | ||||||||||||||||||||
| where | ||||||||||||||||||||
| B: Into<Body>, | ||||||||||||||||||||
| B: IntoBody + 'static, | ||||||||||||||||||||
| { | ||||||||||||||||||||
| fn into_response(self) -> Response<Body> { | ||||||||||||||||||||
| 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<Body> { | ||||||||||||||||||||
| 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<Body> { | ||||||||||||||||||||
| Response::new(Body::from(self)) | ||||||||||||||||||||
| fn into_response(self) -> ResponseFuture { | ||||||||||||||||||||
| Box::pin(ready(Response::new(Body::from(self)))) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl IntoResponse for &[u8] { | ||||||||||||||||||||
| fn into_response(self) -> ResponseFuture { | ||||||||||||||||||||
| Box::pin(ready(Response::new(Body::from(self)))) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl IntoResponse for Vec<u8> { | ||||||||||||||||||||
| fn into_response(self) -> ResponseFuture { | ||||||||||||||||||||
| Box::pin(ready(Response::new(Body::from(self)))) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl IntoResponse for serde_json::Value { | ||||||||||||||||||||
| fn into_response(self) -> Response<Body> { | ||||||||||||||||||||
| 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") | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub type ResponseFuture = Pin<Box<dyn Future<Output = Response<Body>>>>; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| pub trait IntoBody { | ||||||||||||||||||||
| fn into_body(self) -> BodyFuture; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl<B> IntoBody for B | ||||||||||||||||||||
| where | ||||||||||||||||||||
| B: HttpBody + Unpin + 'static, | ||||||||||||||||||||
| B::Error: fmt::Debug, | ||||||||||||||||||||
| { | ||||||||||||||||||||
| fn into_body(self) -> BodyFuture { | ||||||||||||||||||||
| if TypeId::of::<Body>() == self.type_id() { | ||||||||||||||||||||
| let any_self = Box::new(self) as Box<dyn Any + 'static>; | ||||||||||||||||||||
| // Can safely unwrap here as we do type validation in the 'if' statement | ||||||||||||||||||||
| Box::pin(ready(*any_self.downcast::<Body>().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()) }) | |
| Box::pin(async move { | |
| let bytes = to_bytes(self).await.expect("unable to read bytes from body"); | |
| match from_utf8(&bytes) { | |
| Ok(s) => Body::from(s), | |
| Err(_) => Body::from(bytes.to_vec()), | |
| } | |
| }) |
To summarize the code we are commenting on
lambda_http::Bodypass through as they are- Other
bytes::Bytestypes (of whichBoxBodyis)
a. utf8 from bytes ->Body::Text
b. if not ->Body::Binary
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.
@hugobast That's a neat idea. Unfortunately, the fact that the payload is valid UTF-8 does not imply that its contents are text. A priori the user could be sending a PNG that is valid UTF-8 text, for example.
Looking at the Content-Type header of the response is another approach. But again, it's not an infallible solution. The user might not have set the header. It's also easy to map application/json or text/plain to Body::Text, but we can't possibly handle all standard content types, let alone user-defined custom ones.
There are no "one size fits all" solutions unfortunately. All in all, this is why I think it's impossible to land this feature. We can't guess the user's intention. I think any kind of solution will have to ultimately explicitly involve the user communicating his intention to the runtime. Inserting an http::Extensions in the response that the runtime can then read, for example. That will severely limit the user though.
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.
I think any kind of solution will have to ultimately explicitly involve the user communicating his intention to the runtime
I don't think this is necessarily bad.
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.
@bnusunny I think your point needs to be documented in the runtime documentation, but we should not make users depend on specific configurations in another service. binaryMediaTypes are for example not available when you call a function with invoke. People that use that should still be able to use http responses as expected.
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.
For the rules, I would suggest two updates:
2.i.a If it is equal to text/*, application/json, application/javascript or application/xml, leave the body as is and set isBase64Encoded to false.
content-type may have suffixes, such as 'application/json; charset=UTF-8'. So it is better to use:
2.i.a If the content-type begins with text/*, application/json, application/javascript or application/xml, leave the body as is and set isBase64Encoded to false.
2.ii If the
content-typeheader is not present, leave the body as is and setisBase64Encodedtofalse.
This is problematic. If a bad client sent a binary http response without the content-type header, we would treate it as text and Lambda service would throw an error. We should default to binary here. ALB also uses binary as default.
2.ii If the content-type header is not present, base64 encode the body and set isBase64Encoded to true.
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.
@bnusunny I think your point needs to be documented in the runtime documentation, but we should not make users depend on specific configurations in another service. binaryMediaTypes are for example not available when you call a function with
invoke. People that use that should still be able to use http responses as expected.
Yes. We definitely need to document this configuration.
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.
2.ii If the
content-typeheader is not present, leave the body as is and setisBase64Encodedtofalse.
This is problematic. If a bad client sent a binary http response without the
content-typeheader, we would treate it as text and Lambda service would throw an error.
However, if they send a text HTTP response without the content-type header, the client sends e.g. Accept: text/plain and the user has not configured contentHandling (which they shouldn't need to, since they are returning text payloads) we would treat it as binary and return a base64-encoded response. That is, the client would receive eyJtZXNzYWdlIjoiaGVsbG8gc3RyYW5nZXIhIn0= instead of {"message":"hello stranger!"}.
I think APIs that return text are far more common than those that return binary payloads and as such we should default to text in this case where we don't have any information. I also prefer the Lambda service throwing a loud error than the client being puzzled at why they received base64-encoded text.
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.
Make sense. Let's default to text.
By the way. lambda_http crate only supports Lambda Proxy integration with API GW. contentHandling doesn't apply to Lambda Proxy integration. APIGW Developer Guide has the details.
Uh oh!
There was an error while loading. Please reload this page.