Skip to content

Commit 9ee45bf

Browse files
feat: support for server lambda_http::Request (#1551)
Co-authored-by: david-perez <[email protected]>
1 parent 0d1dc51 commit 9ee45bf

File tree

9 files changed

+292
-0
lines changed

9 files changed

+292
-0
lines changed

CHANGELOG.next.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,13 @@ Servers now allow requests' ACCEPT header values to be:
107107
references = ["smithy-rs#1544"]
108108
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "server" }
109109
author = "82marbag"
110+
111+
[[smithy-rs]]
112+
message = """
113+
There is a canonical and easier way to run smithy-rs on Lambda [see example].
114+
115+
[see example]: https://github.com/awslabs/smithy-rs/blob/main/rust-runtime/aws-smithy-http-server/examples/pokemon-service/src/lambda.rs
116+
"""
117+
references = ["smithy-rs#1551"]
118+
meta = { "breaking" = false, "tada" = true, "bug" = false, "target" = "server" }
119+
author = "hugobast"

rust-runtime/aws-smithy-http-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ futures-util = { version = "0.3", default-features = false }
2626
http = "0.2"
2727
http-body = "0.4"
2828
hyper = { version = "0.14.12", features = ["server", "http1", "http2", "tcp", "stream"] }
29+
lambda_http = "0.6.0"
2930
mime = "0.3"
3031
nom = "7"
3132
pin-project-lite = "0.2"

rust-runtime/aws-smithy-http-server/examples/Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ doc-open: codegen
2626
clean:
2727
cargo clean || echo "Unable to run cargo clean"
2828

29+
lambda_watch:
30+
cargo lambda watch
31+
32+
lambda_invoke:
33+
cargo lambda invoke pokemon-service-lambda --data-file pokemon-service/tests/fixtures/example-apigw-request.json
34+
2935
distclean: clean
3036
rm -rf $(SERVER_SDK_DST) $(CLIENT_SDK_DST) Cargo.lock
3137

rust-runtime/aws-smithy-http-server/examples/pokemon-service/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,19 @@ default-run = "pokemon-service"
1111
name = "pokemon-service-tls"
1212
path = "src/bin/pokemon-service-tls.rs"
1313

14+
[[bin]]
15+
name = "pokemon-service"
16+
path = "src/main.rs"
17+
18+
[[bin]]
19+
name = "pokemon-service-lambda"
20+
path = "src/lambda.rs"
21+
1422
[dependencies]
1523
async-stream = "0.3"
1624
clap = { version = "~3.2.1", features = ["derive"] }
1725
hyper = {version = "0.14.12", features = ["server"] }
26+
lambda_http = "0.6.0"
1827
rand = "0.8"
1928
tokio = "1"
2029
tower = "0.4"
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
// This program is exported as a binary named `pokemon-service-lambda`.
7+
use std::sync::Arc;
8+
9+
use aws_smithy_http_server::{routing::LambdaHandler, AddExtensionLayer, Router};
10+
use pokemon_service::{
11+
capture_pokemon, empty_operation, get_pokemon_species, get_server_statistics, get_storage, health_check_operation,
12+
setup_tracing, State,
13+
};
14+
use pokemon_service_server_sdk::operation_registry::OperationRegistryBuilder;
15+
use tower::ServiceBuilder;
16+
use tower_http::trace::TraceLayer;
17+
18+
#[tokio::main]
19+
pub async fn main() {
20+
setup_tracing();
21+
22+
let app: Router = OperationRegistryBuilder::default()
23+
// Build a registry containing implementations to all the operations in the service. These
24+
// are async functions or async closures that take as input the operation's input and
25+
// return the operation's output.
26+
.get_pokemon_species(get_pokemon_species)
27+
.get_storage(get_storage)
28+
.get_server_statistics(get_server_statistics)
29+
.capture_pokemon_operation(capture_pokemon)
30+
.empty_operation(empty_operation)
31+
.health_check_operation(health_check_operation)
32+
.build()
33+
.expect("Unable to build operation registry")
34+
// Convert it into a router that will route requests to the matching operation
35+
// implementation.
36+
.into();
37+
38+
// Setup shared state and middlewares.
39+
let shared_state = Arc::new(State::default());
40+
let app = app.layer(
41+
ServiceBuilder::new()
42+
.layer(TraceLayer::new_for_http())
43+
.layer(AddExtensionLayer::new(shared_state)),
44+
);
45+
46+
let handler = LambdaHandler::new(app);
47+
let lambda = lambda_http::run(handler);
48+
49+
if let Err(err) = lambda.await {
50+
eprintln!("lambda error: {}", err);
51+
}
52+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
{
2+
"body": null,
3+
"headers": {
4+
"Accept": "application/json",
5+
"Accept-Encoding": "gzip, deflate",
6+
"cache-control": "no-cache",
7+
"CloudFront-Forwarded-Proto": "https",
8+
"CloudFront-Is-Desktop-Viewer": "true",
9+
"CloudFront-Is-Mobile-Viewer": "false",
10+
"CloudFront-Is-SmartTV-Viewer": "false",
11+
"CloudFront-Is-Tablet-Viewer": "false",
12+
"CloudFront-Viewer-Country": "US",
13+
"Content-Type": "application/json",
14+
"headerName": "headerValue",
15+
"Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com",
16+
"Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f",
17+
"User-Agent": "PostmanRuntime/2.4.5",
18+
"Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)",
19+
"X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==",
20+
"X-Forwarded-For": "54.240.196.186, 54.182.214.83",
21+
"X-Forwarded-Port": "443",
22+
"X-Forwarded-Proto": "https"
23+
},
24+
"httpMethod": "GET",
25+
"isBase64Encoded": false,
26+
"multiValueHeaders": {
27+
"Accept": ["application/json"],
28+
"Accept-Encoding": ["gzip, deflate"],
29+
"cache-control": ["no-cache"],
30+
"CloudFront-Forwarded-Proto": ["https"],
31+
"CloudFront-Is-Desktop-Viewer": ["true"],
32+
"CloudFront-Is-Mobile-Viewer": ["false"],
33+
"CloudFront-Is-SmartTV-Viewer": ["false"],
34+
"CloudFront-Is-Tablet-Viewer": ["false"],
35+
"CloudFront-Viewer-Country": ["US"],
36+
"Content-Type": ["application/json"],
37+
"headerName": ["headerValue"],
38+
"Host": ["gy415nuibc.execute-api.us-east-1.amazonaws.com"],
39+
"Postman-Token": ["9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f"],
40+
"User-Agent": ["PostmanRuntime/2.4.5"],
41+
"Via": ["1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)"],
42+
"X-Amz-Cf-Id": ["pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A=="],
43+
"X-Forwarded-For": ["54.240.196.186, 54.182.214.83"],
44+
"X-Forwarded-Port": ["443"],
45+
"X-Forwarded-Proto": ["https"]
46+
},
47+
"multiValueQueryStringParameters": {
48+
"key": ["value"]
49+
},
50+
"path": "/stats",
51+
"pathParameters": null,
52+
"queryStringParameters": {
53+
"key": "value"
54+
},
55+
"requestContext": {
56+
"accountId": "xxxxx",
57+
"apiId": "xxxxx",
58+
"domainName": "testPrefix.testDomainName",
59+
"domainPrefix": "testPrefix",
60+
"extendedRequestId": "NvWWKEZbliAFliA=",
61+
"httpMethod": "GET",
62+
"identity": {
63+
"accessKey": "xxxxx",
64+
"accountId": "xxxxx",
65+
"apiKey": "test-invoke-api-key",
66+
"apiKeyId": "test-invoke-api-key-id",
67+
"caller": "xxxxx:xxxxx",
68+
"cognitoAuthenticationProvider": null,
69+
"cognitoAuthenticationType": null,
70+
"cognitoIdentityId": null,
71+
"cognitoIdentityPoolId": null,
72+
"principalOrgId": null,
73+
"sourceIp": "test-invoke-source-ip",
74+
"user": "xxxxx:xxxxx",
75+
"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",
76+
"userArn": "arn:aws:sts::xxxxx:assumed-role/xxxxx/xxxxx"
77+
},
78+
"path": "/stats",
79+
"protocol": "HTTP/1.1",
80+
"requestId": "e5488776-afe4-4e5e-92b1-37bd23f234d6",
81+
"requestTime": "18/Feb/2022:13:23:12 +0000",
82+
"requestTimeEpoch": 1645190592806,
83+
"resourceId": "ddw8yd",
84+
"resourcePath": "/stats",
85+
"stage": "test-invoke-stage"
86+
},
87+
"resource": "/stats",
88+
"stageVariables": null
89+
}

rust-runtime/aws-smithy-http-server/src/rejection.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,6 @@ convert_to_request_rejection!(std::str::Utf8Error, InvalidUtf8);
262262
// tests use `[crate::body::Body]` as their body type when constructing requests (and almost
263263
// everyone will run a Hyper-based server in their services).
264264
convert_to_request_rejection!(hyper::Error, HttpBody);
265+
266+
// Required in order to accept Lambda HTTP requests using `Router<lambda_http::Body>`.
267+
convert_to_request_rejection!(lambda_http::Error, HttpBody);
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
use http::uri;
7+
use lambda_http::{Request, RequestExt};
8+
use std::{
9+
fmt::Debug,
10+
task::{Context, Poll},
11+
};
12+
use tower::Service;
13+
14+
type HyperRequest = http::Request<hyper::Body>;
15+
16+
/// A [`Service`] that takes a `lambda_http::Request` and converts
17+
/// it to `http::Request<hyper::Body>`.
18+
///
19+
/// [`Service`]: tower::Service
20+
#[derive(Debug, Clone)]
21+
pub struct LambdaHandler<S> {
22+
service: S,
23+
}
24+
25+
impl<S> LambdaHandler<S> {
26+
pub fn new(service: S) -> Self {
27+
Self { service }
28+
}
29+
}
30+
31+
impl<S> Service<Request> for LambdaHandler<S>
32+
where
33+
S: Service<HyperRequest>,
34+
{
35+
type Error = S::Error;
36+
type Response = S::Response;
37+
type Future = S::Future;
38+
39+
#[inline]
40+
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
41+
self.service.poll_ready(cx)
42+
}
43+
44+
fn call(&mut self, event: Request) -> Self::Future {
45+
self.service.call(convert_event(event))
46+
}
47+
}
48+
49+
/// Converts a `lambda_http::Request` into a `http::Request<hyper::Body>`
50+
/// Issue: <https://github.com/awslabs/smithy-rs/issues/1125>
51+
///
52+
/// While converting the event the [API Gateway Stage] portion of the URI
53+
/// is removed from the uri that gets returned as a new `http::Request`.
54+
///
55+
/// [API Gateway Stage]: https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-stages.html
56+
fn convert_event(request: Request) -> HyperRequest {
57+
let raw_path = request.raw_http_path();
58+
let (mut parts, body) = request.into_parts();
59+
let mut path = String::from(parts.uri.path());
60+
61+
if !raw_path.is_empty() && raw_path != path {
62+
path = raw_path;
63+
64+
let uri_parts: uri::Parts = parts.uri.into();
65+
let path_and_query = uri_parts
66+
.path_and_query
67+
.expect("request URI does not have `PathAndQuery`");
68+
69+
if let Some(query) = path_and_query.query() {
70+
path.push('?');
71+
path.push_str(query);
72+
}
73+
74+
parts.uri = uri::Uri::builder()
75+
.authority(uri_parts.authority.expect("request URI does not have authority set"))
76+
.scheme(uri_parts.scheme.expect("request URI does not have scheme set"))
77+
.path_and_query(path)
78+
.build()
79+
.expect("unable to construct new URI");
80+
}
81+
82+
let body = match body {
83+
lambda_http::Body::Empty => hyper::Body::empty(),
84+
lambda_http::Body::Text(s) => hyper::Body::from(s),
85+
lambda_http::Body::Binary(v) => hyper::Body::from(v),
86+
};
87+
88+
http::Request::from_parts(parts, body)
89+
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use super::*;
94+
use lambda_http::RequestExt;
95+
96+
#[test]
97+
fn traits() {
98+
use crate::test_helpers::*;
99+
100+
assert_send::<LambdaHandler<()>>();
101+
assert_sync::<LambdaHandler<()>>();
102+
}
103+
104+
#[test]
105+
fn raw_http_path() {
106+
// lambda_http::Request doesn't have a fn `builder`
107+
let event = http::Request::builder()
108+
.uri("https://id.execute-api.us-east-1.amazonaws.com/prod/resources/1")
109+
.body(())
110+
.expect("unable to build Request");
111+
let (parts, _) = event.into_parts();
112+
113+
// the lambda event will have a raw path which is the path without stage name in it
114+
let event =
115+
lambda_http::Request::from_parts(parts, lambda_http::Body::Empty).with_raw_http_path("/resources/1");
116+
let request = convert_event(event);
117+
118+
assert_eq!(request.uri().path(), "/resources/1")
119+
}
120+
}

rust-runtime/aws-smithy-http-server/src/routing/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ use tower_http::map_response_body::MapResponseBodyLayer;
2525

2626
mod future;
2727
mod into_make_service;
28+
mod lambda_handler;
2829

2930
#[doc(hidden)]
3031
pub mod request_spec;
3132

3233
mod route;
3334
mod tiny_map;
3435

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

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

0 commit comments

Comments
 (0)