-
Notifications
You must be signed in to change notification settings - Fork 361
feat(lambda-http): accept http_body::Body in responses #466
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 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,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<Response<Body>, Error> { | ||
Ok("Success".into_response().await) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<impl IntoResponse, Error> { | ||
Ok((200, "Hello, world!")) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<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 +95,103 @@ 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; | ||
} | ||
|
||
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) }; | ||
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. why does this have to be a future? 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. Here, this is due to the 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. Is it possible to collect partial payloads? Thinking HTTP Ranges or terminate early if a piece of data has been seen in the response as it streams instead of waiting for the whole payload? Not sure if this use case should be available through this API or just limited to a custom hack, but it would serve my interests. 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. @brainstorm Not immediately for this purpose, since here we just use this to transform it into a That said, if you're calling an API that returns an |
||
|
||
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 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") | ||
}) | ||
} | ||
} | ||
|
||
impl<S, B> IntoResponse for (S, B) | ||
where | ||
S: TryInto<StatusCode> + 'static, | ||
S::Error: fmt::Debug, | ||
B: Into<Body> + 'static, | ||
{ | ||
fn into_response(self) -> ResponseFuture { | ||
Box::pin(async move { | ||
Response::builder() | ||
.status(self.0.try_into().expect("unable to transform status code")) | ||
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. This will panic if the conversion fails. I'm not sure if this convenience blanket impl is worth panicking. At the very least it should be documented. |
||
.body(self.1.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()) }) | ||
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. This will always use the See https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings-workflow.html, which makes my head hurt. 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. ALB integration use content-encoding and content-type headers to determine if the body should be base64 encoded. See it here: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#receive-event-from-load-balancer Perhaps we could add utility methods to convert This means we should probably work on |
||
} | ||
} | ||
} | ||
|
||
pub type BodyFuture = Pin<Box<dyn Future<Output = Body>>>; | ||
|
||
#[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"), | ||
|
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.
nit: since only one of these will be polled at a time, it might be better to model it as an enum with two variants.
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 need some help here @calavera, @eduardomourar or @david-perez
The PR as it is now simply doesn't work. The lambdas using
lambda_http
get called (I can see my info logs with the event) but never return the response and time out.I believe this
poll
function is at the core of the issue: https://github.com/awslabs/aws-lambda-rust-runtime/pull/466/files#diff-c62692e6b817476306b4d6966f541eec77a1768674e4dad5cf7f10c4faababa4R106-R119I observed that the value of
self.fut_res
gets set then nothing else happens, thepoll
function never gets executed again with the value ofself.fut_res
set, it actually looks like it gets called once then that's it.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.
Ok bear with me I think I know why this isn't working as-is: https://tokio.rs/tokio/tutorial/async#wakers
Looks like by returning
Poll::Pending
fromPoll::Ready
and doing nothing else we are breaking the contract we also need to take an additional stepThe code should be:
And I've confirmed now that this works correctly. Continuing on with testing.
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 Good catch! This is indeed a sharp edge of async Rust.
Typically only leaf futures are the ones that interact with wakers. Most futures uphold the
poll
contract by returningPoll::Pending
only if some other future also returnedPoll::Pending
; in this way, it trivially fulfills the contract ofpoll
since the inner future must follow that same contract.In this particular case, we can do the same by polling the future that we just put in
self.fut_res
. That is, do here what is done in the first arm of the if statement too.I think it's clearer if we model this as an enum, since we're only polling one of the futures at a time, and simply do
self.poll()
when the request future completes and we now want to poll the response future.(Note though that your code is perfectly fine in terms of correctness)
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.
@david-perez something like this? hugobast@d58a1bc
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.
that looks good to me