Skip to content

Commit

Permalink
feat: support for server lambda_http::Request (#1551)
Browse files Browse the repository at this point in the history
Co-authored-by: david-perez <[email protected]>
  • Loading branch information
hugobast and david-perez authored Aug 19, 2022
1 parent 0d1dc51 commit 9ee45bf
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 0 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.next.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,13 @@ Servers now allow requests' ACCEPT header values to be:
references = ["smithy-rs#1544"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "server" }
author = "82marbag"

[[smithy-rs]]
message = """
There is a canonical and easier way to run smithy-rs on Lambda [see example].
[see example]: https://github.com/awslabs/smithy-rs/blob/main/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/lambda.rs
"""
references = ["smithy-rs#1551"]
meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "server" }
author = "hugobast"
1 change: 1 addition & 0 deletions rust-runtime/aws-smithy-http-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ futures-util = { version = "0.3", default-features = false }
http = "0.2"
http-body = "0.4"
hyper = { version = "0.14.12", features = ["server", "http1", "http2", "tcp", "stream"] }
lambda_http = "0.6.0"
mime = "0.3"
nom = "7"
pin-project-lite = "0.2"
Expand Down
6 changes: 6 additions & 0 deletions rust-runtime/aws-smithy-http-server/examples/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ doc-open: codegen
clean:
cargo clean || echo "Unable to run cargo clean"

lambda_watch:
cargo lambda watch

lambda_invoke:
cargo lambda invoke pokemon-service-lambda --data-file pokemon-service/tests/fixtures/example-apigw-request.json

distclean: clean
rm -rf $(SERVER_SDK_DST) $(CLIENT_SDK_DST) Cargo.lock

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,19 @@ default-run = "pokemon-service"
name = "pokemon-service-tls"
path = "src/bin/pokemon-service-tls.rs"

[[bin]]
name = "pokemon-service"
path = "src/main.rs"

[[bin]]
name = "pokemon-service-lambda"
path = "src/lambda.rs"

[dependencies]
async-stream = "0.3"
clap = { version = "~3.2.1", features = ["derive"] }
hyper = {version = "0.14.12", features = ["server"] }
lambda_http = "0.6.0"
rand = "0.8"
tokio = "1"
tower = "0.4"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

// This program is exported as a binary named `pokemon-service-lambda`.
use std::sync::Arc;

use aws_smithy_http_server::{routing::LambdaHandler, AddExtensionLayer, Router};
use pokemon_service::{
capture_pokemon, empty_operation, get_pokemon_species, get_server_statistics, get_storage, health_check_operation,
setup_tracing, State,
};
use pokemon_service_server_sdk::operation_registry::OperationRegistryBuilder;
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;

#[tokio::main]
pub async fn main() {
setup_tracing();

let app: Router = OperationRegistryBuilder::default()
// Build a registry containing implementations to all the operations in the service. These
// are async functions or async closures that take as input the operation's input and
// return the operation's output.
.get_pokemon_species(get_pokemon_species)
.get_storage(get_storage)
.get_server_statistics(get_server_statistics)
.capture_pokemon_operation(capture_pokemon)
.empty_operation(empty_operation)
.health_check_operation(health_check_operation)
.build()
.expect("Unable to build operation registry")
// Convert it into a router that will route requests to the matching operation
// implementation.
.into();

// Setup shared state and middlewares.
let shared_state = Arc::new(State::default());
let app = app.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(AddExtensionLayer::new(shared_state)),
);

let handler = LambdaHandler::new(app);
let lambda = lambda_http::run(handler);

if let Err(err) = lambda.await {
eprintln!("lambda error: {}", err);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"body": null,
"headers": {
"Accept": "application/json",
"Accept-Encoding": "gzip, deflate",
"cache-control": "no-cache",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "US",
"Content-Type": "application/json",
"headerName": "headerValue",
"Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com",
"Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f",
"User-Agent": "PostmanRuntime/2.4.5",
"Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==",
"X-Forwarded-For": "54.240.196.186, 54.182.214.83",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
"httpMethod": "GET",
"isBase64Encoded": false,
"multiValueHeaders": {
"Accept": ["application/json"],
"Accept-Encoding": ["gzip, deflate"],
"cache-control": ["no-cache"],
"CloudFront-Forwarded-Proto": ["https"],
"CloudFront-Is-Desktop-Viewer": ["true"],
"CloudFront-Is-Mobile-Viewer": ["false"],
"CloudFront-Is-SmartTV-Viewer": ["false"],
"CloudFront-Is-Tablet-Viewer": ["false"],
"CloudFront-Viewer-Country": ["US"],
"Content-Type": ["application/json"],
"headerName": ["headerValue"],
"Host": ["gy415nuibc.execute-api.us-east-1.amazonaws.com"],
"Postman-Token": ["9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f"],
"User-Agent": ["PostmanRuntime/2.4.5"],
"Via": ["1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)"],
"X-Amz-Cf-Id": ["pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A=="],
"X-Forwarded-For": ["54.240.196.186, 54.182.214.83"],
"X-Forwarded-Port": ["443"],
"X-Forwarded-Proto": ["https"]
},
"multiValueQueryStringParameters": {
"key": ["value"]
},
"path": "/stats",
"pathParameters": null,
"queryStringParameters": {
"key": "value"
},
"requestContext": {
"accountId": "xxxxx",
"apiId": "xxxxx",
"domainName": "testPrefix.testDomainName",
"domainPrefix": "testPrefix",
"extendedRequestId": "NvWWKEZbliAFliA=",
"httpMethod": "GET",
"identity": {
"accessKey": "xxxxx",
"accountId": "xxxxx",
"apiKey": "test-invoke-api-key",
"apiKeyId": "test-invoke-api-key-id",
"caller": "xxxxx:xxxxx",
"cognitoAuthenticationProvider": null,
"cognitoAuthenticationType": null,
"cognitoIdentityId": null,
"cognitoIdentityPoolId": null,
"principalOrgId": null,
"sourceIp": "test-invoke-source-ip",
"user": "xxxxx:xxxxx",
"userAgent": "aws-internal/3 aws-sdk-java/1.12.154 Linux/5.4.156-94.273.amzn2int.x86_64 OpenJDK_64-Bit_Server_VM/25.322-b06 java/1.8.0_322 vendor/Oracle_Corporation cfg/retry-mode/standard",
"userArn": "arn:aws:sts::xxxxx:assumed-role/xxxxx/xxxxx"
},
"path": "/stats",
"protocol": "HTTP/1.1",
"requestId": "e5488776-afe4-4e5e-92b1-37bd23f234d6",
"requestTime": "18/Feb/2022:13:23:12 +0000",
"requestTimeEpoch": 1645190592806,
"resourceId": "ddw8yd",
"resourcePath": "/stats",
"stage": "test-invoke-stage"
},
"resource": "/stats",
"stageVariables": null
}
3 changes: 3 additions & 0 deletions rust-runtime/aws-smithy-http-server/src/rejection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,6 @@ convert_to_request_rejection!(std::str::Utf8Error, InvalidUtf8);
// tests use `[crate::body::Body]` as their body type when constructing requests (and almost
// everyone will run a Hyper-based server in their services).
convert_to_request_rejection!(hyper::Error, HttpBody);

// Required in order to accept Lambda HTTP requests using `Router<lambda_http::Body>`.
convert_to_request_rejection!(lambda_http::Error, HttpBody);
120 changes: 120 additions & 0 deletions rust-runtime/aws-smithy-http-server/src/routing/lambda_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

use http::uri;
use lambda_http::{Request, RequestExt};
use std::{
fmt::Debug,
task::{Context, Poll},
};
use tower::Service;

type HyperRequest = http::Request<hyper::Body>;

/// A [`Service`] that takes a `lambda_http::Request` and converts
/// it to `http::Request<hyper::Body>`.
///
/// [`Service`]: tower::Service
#[derive(Debug, Clone)]
pub struct LambdaHandler<S> {
service: S,
}

impl<S> LambdaHandler<S> {
pub fn new(service: S) -> Self {
Self { service }
}
}

impl<S> Service<Request> for LambdaHandler<S>
where
S: Service<HyperRequest>,
{
type Error = S::Error;
type Response = S::Response;
type Future = S::Future;

#[inline]
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}

fn call(&mut self, event: Request) -> Self::Future {
self.service.call(convert_event(event))
}
}

/// Converts a `lambda_http::Request` into a `http::Request<hyper::Body>`
/// Issue: <https://github.com/awslabs/smithy-rs/issues/1125>
///
/// While converting the event the [API Gateway Stage] portion of the URI
/// is removed from the uri that gets returned as a new `http::Request`.
///
/// [API Gateway Stage]: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-stages.html
fn convert_event(request: Request) -> HyperRequest {
let raw_path = request.raw_http_path();
let (mut parts, body) = request.into_parts();
let mut path = String::from(parts.uri.path());

if !raw_path.is_empty() && raw_path != path {
path = raw_path;

let uri_parts: uri::Parts = parts.uri.into();
let path_and_query = uri_parts
.path_and_query
.expect("request URI does not have `PathAndQuery`");

if let Some(query) = path_and_query.query() {
path.push('?');
path.push_str(query);
}

parts.uri = uri::Uri::builder()
.authority(uri_parts.authority.expect("request URI does not have authority set"))
.scheme(uri_parts.scheme.expect("request URI does not have scheme set"))
.path_and_query(path)
.build()
.expect("unable to construct new URI");
}

let body = match body {
lambda_http::Body::Empty => hyper::Body::empty(),
lambda_http::Body::Text(s) => hyper::Body::from(s),
lambda_http::Body::Binary(v) => hyper::Body::from(v),
};

http::Request::from_parts(parts, body)
}

#[cfg(test)]
mod tests {
use super::*;
use lambda_http::RequestExt;

#[test]
fn traits() {
use crate::test_helpers::*;

assert_send::<LambdaHandler<()>>();
assert_sync::<LambdaHandler<()>>();
}

#[test]
fn raw_http_path() {
// lambda_http::Request doesn't have a fn `builder`
let event = http::Request::builder()
.uri("https://id.execute-api.us-east-1.amazonaws.com/prod/resources/1")
.body(())
.expect("unable to build Request");
let (parts, _) = event.into_parts();

// the lambda event will have a raw path which is the path without stage name in it
let event =
lambda_http::Request::from_parts(parts, lambda_http::Body::Empty).with_raw_http_path("/resources/1");
let request = convert_event(event);

assert_eq!(request.uri().path(), "/resources/1")
}
}
2 changes: 2 additions & 0 deletions rust-runtime/aws-smithy-http-server/src/routing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ use tower_http::map_response_body::MapResponseBodyLayer;

mod future;
mod into_make_service;
mod lambda_handler;

#[doc(hidden)]
pub mod request_spec;

mod route;
mod tiny_map;

pub use self::lambda_handler::LambdaHandler;
pub use self::{future::RouterFuture, into_make_service::IntoMakeService, route::Route};

/// The router is a [`tower::Service`] that routes incoming requests to other `Service`s
Expand Down

0 comments on commit 9ee45bf

Please sign in to comment.