diff --git a/rust-runtime/aws-smithy-http-server/Cargo.toml b/rust-runtime/aws-smithy-http-server/Cargo.toml index 48f11d30f93..f0b92ddf740 100644 --- a/rust-runtime/aws-smithy-http-server/Cargo.toml +++ b/rust-runtime/aws-smithy-http-server/Cargo.toml @@ -24,6 +24,7 @@ futures-util = { version = "0.3", default-features = false } http = "0.2" http-body = "0.4" hyper = { version = "0.14", features = ["server", "http1", "http2", "tcp", "stream"] } +lambda_http = "0.5" mime = "0.3" nom = "7" paste = "1" @@ -35,9 +36,11 @@ thiserror = "1" tokio = { version = "1.0", features = ["full"] } tower = { version = "0.4.11", features = ["util", "make"], default-features = false } tower-http = { version = "0.3", features = ["add-extension", "map-response-body"] } +tracing = "0.1" [dev-dependencies] pretty_assertions = "1" +serde_json = { version = "1", default-features = false, features = ["alloc"] } [package.metadata.docs.rs] all-features = true diff --git a/rust-runtime/aws-smithy-http-server/examples/Makefile b/rust-runtime/aws-smithy-http-server/examples/Makefile index 9eb98c07b04..a95b7b0a5ab 100644 --- a/rust-runtime/aws-smithy-http-server/examples/Makefile +++ b/rust-runtime/aws-smithy-http-server/examples/Makefile @@ -23,6 +23,12 @@ run: codegen doc-open: codegen cargo doc --no-deps --open +lambda_watch: + cargo lambda watch + +lambda_invoke: + cargo lambda invoke bootstrap --data-file pokemon_service/tests/fixtures/example-apigw-request.json + clean: cargo clean || echo "Unable to run cargo clean" diff --git a/rust-runtime/aws-smithy-http-server/examples/README.md b/rust-runtime/aws-smithy-http-server/examples/README.md index 28a9cff5cfc..749e6616cd7 100644 --- a/rust-runtime/aws-smithy-http-server/examples/README.md +++ b/rust-runtime/aws-smithy-http-server/examples/README.md @@ -15,18 +15,32 @@ can be used directly. `make distclean` can be used for a complete cleanup of all artefacts. -## Run +## On Hyper server + +### Run `cargo run` can be used to start the Pokémon service on `http://localhost:13734`. -## Test +### Test `cargo test` can be used to spawn the service and run some simple integration tests against it. More info can be found in the `tests` folder of `pokemon_service` package. +## On Lambda + +### Run + +`make lambda_watch` can be used to start the Pokémon service using the +Lambda emulator provided by https://github.com/cargo-lambda/cargo-lambda. + +### Test + +`make lambda_invoke` can be used to run some simple request based on a +API Gateway sample payload. + ## Benchmarks Please see [BENCHMARKS.md](/rust-runtime/aws-smithy-http-server/examples/BENCHMARKS.md). diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon_service/Cargo.toml b/rust-runtime/aws-smithy-http-server/examples/pokemon_service/Cargo.toml index a503537f431..37e790bd510 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon_service/Cargo.toml +++ b/rust-runtime/aws-smithy-http-server/examples/pokemon_service/Cargo.toml @@ -9,6 +9,7 @@ description = "A smithy Rust service to retrieve information about Pokémon." [dependencies] clap = { version = "3.2.1", features = ["derive"] } hyper = {version = "0.14", features = ["server"] } +lambda_http = "0.5" tokio = "1" tower = "0.4" tower-http = { version = "0.3", features = ["trace"] } @@ -27,3 +28,10 @@ wrk-api-bench = "0.0.7" # Local paths aws-smithy-client = { path = "../../../aws-smithy-client/", features = ["rustls"] } pokemon_service_client = { path = "../pokemon_service_client/" } + +[[bin]] +name = "bootstrap" +path = "src/main.rs" + +[package.metadata.lambda.env] +RUST_LOG = "smithy_http_tower::dispatch=debug,smithy_http::middleware=debug,aws_smithy_http_server=debug" diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon_service/src/main.rs b/rust-runtime/aws-smithy-http-server/examples/pokemon_service/src/main.rs index e84559daa38..d1bfa203f21 100644 --- a/rust-runtime/aws-smithy-http-server/examples/pokemon_service/src/main.rs +++ b/rust-runtime/aws-smithy-http-server/examples/pokemon_service/src/main.rs @@ -7,12 +7,19 @@ use std::{net::SocketAddr, sync::Arc}; use aws_smithy_http_server::{AddExtensionLayer, Router}; +#[allow(unused_imports)] +use lambda_http::{run as run_on_lambda, RequestExt as _}; use clap::Parser; use pokemon_service::{empty_operation, get_pokemon_species, get_server_statistics, setup_tracing, State}; use pokemon_service_sdk::operation_registry::OperationRegistryBuilder; use tower::ServiceBuilder; use tower_http::trace::TraceLayer; +/// Returns true if it is running on AWS Lambda +pub fn is_lambda_environment() -> bool { + std::env::var("AWS_LAMBDA_RUNTIME_API").is_ok() +} + #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Args { @@ -49,14 +56,22 @@ pub async fn main() { .layer(AddExtensionLayer::new(shared_state)), ); - // Start the [`hyper::Server`]. - let bind: SocketAddr = format!("{}:{}", args.address, args.port) - .parse() - .expect("unable to parse the server bind address and port"); - let server = hyper::Server::bind(&bind).serve(app.into_make_service()); + if is_lambda_environment() { + // Start Lambda + let lambda = run_on_lambda(app.into_make_lambda_service()); + if let Err(err) = lambda.await { + eprintln!("lambda error: {}", err); + } + } else { + // Start the [`hyper::Server`]. + let bind: SocketAddr = format!("{}:{}", args.address, args.port) + .parse() + .expect("unable to parse the server bind address and port"); + let server = hyper::Server::bind(&bind).serve(app.into_make_service()); - // Run forever-ish... - if let Err(err) = server.await { - eprintln!("server error: {}", err); + // Run forever-ish... + if let Err(err) = server.await { + eprintln!("server error: {}", err); + } } } diff --git a/rust-runtime/aws-smithy-http-server/examples/pokemon_service/tests/fixtures/example-apigw-request.json b/rust-runtime/aws-smithy-http-server/examples/pokemon_service/tests/fixtures/example-apigw-request.json new file mode 100644 index 00000000000..dde18232bc2 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/examples/pokemon_service/tests/fixtures/example-apigw-request.json @@ -0,0 +1,129 @@ +{ + "body": null, + "headers": { + "Accept": "*/*", + "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": [ + "*/*" + ], + "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 +} diff --git a/rust-runtime/aws-smithy-http-server/src/routing/into_make_lambda_service.rs b/rust-runtime/aws-smithy-http-server/src/routing/into_make_lambda_service.rs new file mode 100644 index 00000000000..6bf494d54ee --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/src/routing/into_make_lambda_service.rs @@ -0,0 +1,203 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +// This code was heavily inspired by https://github.com/hanabu/lambda-web + +use crate::BoxError; +use futures_util::future::BoxFuture; +use http::{uri, Response}; +use http_body::Body; +use hyper::Body as HyperBody; +#[allow(unused_imports)] +use lambda_http::{Body as LambdaBody, Request, RequestExt as _}; +use std::{ + convert::Infallible, + error::Error, + fmt::Debug, + task::{Context, Poll}, +}; +use tower::Service; + +type HyperRequest = http::Request; + +/// A [`MakeService`] that produces AWS Lambda compliant services. +/// +/// [`MakeService`]: tower::make::MakeService +#[derive(Debug, Clone)] +pub struct IntoMakeLambdaService { + service: S, +} + +impl IntoMakeLambdaService { + pub(super) fn new(service: S) -> Self { + Self { service } + } +} + +impl Service for IntoMakeLambdaService +where + S: Service, Error = Infallible> + Clone + Send + 'static, + S::Future: Send + 'static, + B: Body + Send + Debug, + ::Error: Error + Send + Sync + 'static, + ::Data: Send, +{ + type Error = BoxError; + type Response = Response; + type Future = MakeRouteLambdaServiceFuture; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx).map_err(|err| err.into()) + } + + /// Lambda handler function + /// Parse Lambda request as hyper request, + /// serialize hyper response to Lambda response + fn call(&mut self, event: Request) -> Self::Future { + // As recommended in https://github.com/tower-rs/tower/issues/547 + let clone = self.service.clone(); + let mut inner = std::mem::replace(&mut self.service, clone); + + let fut = async move { + // Parse request + let hyper_request = lambda_to_hyper_request(event)?; + + // Call Hyper service when request parsing succeeded + let response = inner.call(hyper_request).await?; + + // Return Lambda response after finished calling inner service + hyper_to_lambda_response(response).await + }; + MakeRouteLambdaServiceFuture::new(Box::pin(fut)) + } +} + +opaque_future! { + /// Response future for [`IntoMakeLambdaService`] services. + pub type MakeRouteLambdaServiceFuture = BoxFuture<'static, Result, BoxError>>; +} + +fn lambda_to_hyper_request(request: Request) -> Result { + tracing::debug!("Converting Lambda to Hyper request..."); + // Raw HTTP path without any stage information + 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 { + tracing::debug!("Recreating URI from raw HTTP path."); + path = raw_path; + let uri_parts: uri::Parts = parts.uri.into(); + let path_and_query = uri_parts.path_and_query.unwrap(); + if let Some(query) = path_and_query.query() { + path.push('?'); + path.push_str(query); + } + parts.uri = uri::Uri::builder() + .authority(uri_parts.authority.unwrap()) + .scheme(uri_parts.scheme.unwrap()) + .path_and_query(path) + .build() + .unwrap(); + } + let body = match body { + LambdaBody::Empty => HyperBody::empty(), + LambdaBody::Text(s) => HyperBody::from(s), + LambdaBody::Binary(v) => HyperBody::from(v), + }; + // We need to maintain all parts including the extensions + let req = http::Request::from_parts(parts, body); + tracing::debug!("Hyper request converted successfully."); + Ok(req) +} + +async fn hyper_to_lambda_response(response: Response) -> Result, BoxError> +where + B: Body + Debug, + ::Error: Error + Send + Sync + 'static, +{ + tracing::debug!("Converting Hyper to Lambda response..."); + // Divide response into headers and body + let (parts, body) = response.into_parts(); + let body = hyper::body::to_bytes(body).await?; + let res = Response::from_parts(parts, LambdaBody::from(body.as_ref())); + tracing::debug!("Lambda response converted successfully."); + Ok(res) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{body::to_boxed, test_helpers::get_body_as_string}; + use http::header::CONTENT_TYPE; + use hyper::Response; + use lambda_http::request::LambdaRequest; + use serde_json::error::Error as JsonError; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map(LambdaRequest::into) + } + + #[test] + fn traits() { + use crate::test_helpers::*; + + assert_send::>(); + assert_sync::>(); + } + + #[test] + fn converts_apigw_event_to_hyper_request() { + let input = include_str!("../../test_data/apigw_request.json"); + let req = from_str(input).expect("failed to parse request"); + let result = lambda_to_hyper_request(req).expect("failed to convert to hyper request"); + assert_eq!(result.method(), "GET"); + assert_eq!( + result.uri(), + "https://wt6mne2s9k.execute-api.us-west-2.amazonaws.com/hello?name=me" + ); + // assert_eq!(result.raw_http_path(), "/hello"); + } + + #[tokio::test] + async fn converts_apigw_v2_event_to_hyper_request() { + let input = include_str!("../../test_data/apigw_v2_request.json"); + let req = from_str(input).expect("failed to parse request"); + let mut result = lambda_to_hyper_request(req).expect("failed to convert to hyper request"); + assert_eq!(result.method(), "POST"); + assert_eq!(result.uri(), "https://id.execute-api.us-east-1.amazonaws.com/my/path?parameter1=value1¶meter1=value2¶meter2=value"); + assert_eq!( + result + .headers() + .get(CONTENT_TYPE) + .map(|h| h.to_str().expect("invalid header")), + Some("application/json") + ); + let actual_body = get_body_as_string(result.body_mut()).await; + assert_eq!(actual_body, r##"{"message":"Hello from Lambda"}"##); + } + + #[tokio::test] + async fn converts_hyper_to_lambda_response() { + let builder = Response::builder() + .status(200) + .header(CONTENT_TYPE, "application/json") + .body(to_boxed("{\"hello\":\"lambda\"}")); + let res = builder.expect("failed to parse response"); + let mut result = hyper_to_lambda_response(res) + .await + .expect("failed to convert to lambda request"); + assert_eq!(result.status(), 200); + assert_eq!( + result + .headers() + .get(CONTENT_TYPE) + .map(|h| h.to_str().expect("invalid header")), + Some("application/json") + ); + let actual_body = get_body_as_string(result.body_mut()).await; + assert_eq!(actual_body, r##"{"hello":"lambda"}"##); + } +} diff --git a/rust-runtime/aws-smithy-http-server/src/routing/mod.rs b/rust-runtime/aws-smithy-http-server/src/routing/mod.rs index 2f4fa6c94f1..2ca01de912e 100644 --- a/rust-runtime/aws-smithy-http-server/src/routing/mod.rs +++ b/rust-runtime/aws-smithy-http-server/src/routing/mod.rs @@ -26,6 +26,7 @@ use tower::{Service, ServiceBuilder}; use tower_http::map_response_body::MapResponseBodyLayer; mod future; +mod into_make_lambda_service; mod into_make_service; #[doc(hidden)] @@ -34,7 +35,7 @@ pub mod request_spec; mod route; mod tiny_map; -pub use self::{into_make_service::IntoMakeService, route::Route}; +pub use self::{into_make_lambda_service::IntoMakeLambdaService, into_make_service::IntoMakeService, route::Route}; /// The router is a [`tower::Service`] that routes incoming requests to other `Service`s /// based on the request's URI and HTTP method or on some specific header setting the target operation. @@ -139,6 +140,18 @@ where IntoMakeService::new(self) } + /// Convert this router into a [`MakeService`], that is a [`Service`] whose + /// response is another service. + /// + /// This is useful when running your application with AWS Lambda's + /// [`run`]. + /// + /// [`run`]: lambda_http::run + /// [`MakeService`]: tower::make::MakeService + pub fn into_make_lambda_service(self) -> IntoMakeLambdaService { + IntoMakeLambdaService::new(self) + } + /// Apply a [`tower::Layer`] to the router. /// /// All requests to the router will be processed by the layer's @@ -371,10 +384,7 @@ where #[cfg(test)] mod rest_tests { use super::*; - use crate::{ - body::{boxed, BoxBody}, - routing::request_spec::*, - }; + use crate::{body::{boxed, BoxBody}, routing::request_spec::*, test_helpers::get_body_as_string}; use futures_util::Future; use http::{HeaderMap, Method}; use std::pin::Pin; @@ -388,17 +398,6 @@ mod rest_tests { r } - // Returns a `Response`'s body as a `String`, without consuming the response. - pub async fn get_body_as_string(res: &mut Response) -> String - where - B: http_body::Body + std::marker::Unpin, - B::Error: std::fmt::Debug, - { - let body_mut = res.body_mut(); - let body_bytes = hyper::body::to_bytes(body_mut).await.unwrap(); - String::from(std::str::from_utf8(&body_bytes).unwrap()) - } - /// A service that returns its name and the request's URI in the response body. #[derive(Clone)] struct NamedEchoUriService(String); @@ -501,7 +500,7 @@ mod rest_tests { ]; for (svc_name, method, uri) in &hits { let mut res = router.call(req(method, uri, None)).await.unwrap(); - let actual_body = get_body_as_string(&mut res).await; + let actual_body = get_body_as_string(res.body_mut()).await; assert_eq!(format!("{} :: {}", svc_name, uri), actual_body); } @@ -597,9 +596,9 @@ mod rest_tests { #[cfg(test)] mod awsjson_tests { - use super::rest_tests::{get_body_as_string, req}; + use super::rest_tests::req; use super::*; - use crate::body::boxed; + use crate::{body::boxed, test_helpers::get_body_as_string}; use futures_util::Future; use http::{HeaderMap, HeaderValue, Method}; use pretty_assertions::assert_eq; @@ -657,7 +656,7 @@ mod awsjson_tests { .call(req(&Method::POST, "/", Some(headers.clone()))) .await .unwrap(); - let actual_body = get_body_as_string(&mut res).await; + let actual_body = get_body_as_string(res.body_mut()).await; assert_eq!(format!("{} :: {}", "A", "Service.Operation"), actual_body); // No headers, should return NOT_FOUND. diff --git a/rust-runtime/aws-smithy-http-server/src/test_helpers.rs b/rust-runtime/aws-smithy-http-server/src/test_helpers.rs index 8a115abb137..9c34ff332d4 100644 --- a/rust-runtime/aws-smithy-http-server/src/test_helpers.rs +++ b/rust-runtime/aws-smithy-http-server/src/test_helpers.rs @@ -5,3 +5,12 @@ pub(crate) fn assert_send() {} pub(crate) fn assert_sync() {} +// Returns a HTTP body as a `String`, without consuming the request or response. +pub(crate) async fn get_body_as_string(body: &mut B) -> String +where + B: http_body::Body + std::marker::Unpin, + B::Error: std::fmt::Debug, +{ + let body_bytes = hyper::body::to_bytes(body).await.unwrap(); + String::from(std::str::from_utf8(&body_bytes).unwrap()) +} diff --git a/rust-runtime/aws-smithy-http-server/test_data/apigw_request.json b/rust-runtime/aws-smithy-http-server/test_data/apigw_request.json new file mode 100644 index 00000000000..6ce3113d866 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/test_data/apigw_request.json @@ -0,0 +1,56 @@ +{ + "path": "/hello", + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, lzma, sdch, br", + "Accept-Language": "en-US,en;q=0.8", + "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", + "Host": "wt6mne2s9k.execute-api.us-west-2.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48", + "Via": "1.1 fb7cca60f0ecd82ce07790c9c5eef16c.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "nBsWBOrSHMgnaROZJK1wGCZ9PcRcSpq_oSXZNQwQ10OTZL4cimZo3g==", + "X-Forwarded-For": "192.168.100.1, 192.168.1.1", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "pathParameters": { + "proxy": "hello" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "us4z18", + "stage": "test", + "requestId": "41b45ea3-70b5-11e6-b7bd-69b5aaebc7d9", + "requestTimeEpoch": 1583798639428, + "identity": { + "cognitoIdentityPoolId": "", + "accountId": "", + "cognitoIdentityId": "", + "caller": "", + "apiKey": "", + "sourceIp": "192.168.100.1", + "cognitoAuthenticationType": "", + "cognitoAuthenticationProvider": "", + "userArn": "", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36 OPR/39.0.2256.48", + "user": "" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "wt6mne2s9k" + }, + "resource": "/{proxy+}", + "httpMethod": "GET", + "queryStringParameters": { + "name": "me" + }, + "stageVariables": { + "stageVarName": "stageVarValue" + } +} diff --git a/rust-runtime/aws-smithy-http-server/test_data/apigw_v2_request.json b/rust-runtime/aws-smithy-http-server/test_data/apigw_v2_request.json new file mode 100644 index 00000000000..612bbdbb1e3 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server/test_data/apigw_v2_request.json @@ -0,0 +1,58 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1=value1", + "cookie2=value2" + ], + "headers": { + "content-type": "application/json", + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "{\"message\":\"Hello from Lambda\"}", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +}