From 2e265fe33fa54bac6c69b1b0048177bb53e3aa4f Mon Sep 17 00:00:00 2001 From: Daniele <> Date: Sat, 22 Oct 2022 09:51:46 +0200 Subject: [PATCH 1/6] add readme --- lambda-http/README.md | 172 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 lambda-http/README.md diff --git a/lambda-http/README.md b/lambda-http/README.md new file mode 100644 index 00000000..19533fa2 --- /dev/null +++ b/lambda-http/README.md @@ -0,0 +1,172 @@ +# lambda-http for AWS Lambda in Rust + +[![Docs](https://docs.rs/lambda_http/badge.svg)](https://docs.rs/lambda_http) + +**`lambda-http`** is an abstraction that takes payloads from different services and turns them into http objects, making it easy to write API Gateway proxy event focused Lambda functions in Rust. + +lambda-http handler is made of: +* Request - Represents an HTTP request +* IntoResponse - Future that will convert an [`IntoResponse`] into an actual [`LambdaResponse`] + +We are able to handle requests from: +* [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) REST and HTTP API lambda integrations +* AWS [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) + +Thanks to the Request type we can seemsly handle proxy integrations without the worry to specify the specific service type. + +There is also an Extentions for `lambda_http::Request` structs that provide access to [API gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) and [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html) features. + +For example some handy extensions: + +* query_string_parameters - Return pre-parsed http query string parameters, parameters provided after the `?` portion of a url associated with the request +* path_parameters - Return pre-extracted path parameters, parameter provided in url placeholders `/foo/{bar}/baz/{boom}` associated with the request +* payload - Return the Result of a payload parsed into a serde Deserializeable type + +## Examples + +Here you will find a few examples to handle basic scenarions: + +* Reading a JSON from a body and deserialise into a structure +* Reading querystring parameters +* Lambda Request Authorizer + +### Reading a JSON from a body and deserialise into a structure + +The code below creates a simple API Gateway proxy (HTTP, REST) that accept in input a JSON payload. + +```rust +use lambda_http::{http::StatusCode, service_fn, Error, IntoResponse, Request}; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_ansi(false) + .without_time() + .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + .init(); + + lambda_http::run(service_fn(|event: Request| handler(event))).await?; + Ok(()) +} + +pub async fn execute(event: Request) -> Result { + let body = event.payload::()?; + + let response = Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(json!({ + "message": "Hello World", + "payload": body, + }).to_string()) + .unwrap(); + + Ok(response) +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct MyPayload { + pub prop1: String, + pub prop2: String, +} +``` + +### Reading querystring parameters + +```rust +use lambda_http::{http::StatusCode, service_fn, Error, IntoResponse, Request, RequestExt}; + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_ansi(false) + .without_time() + .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + .init(); + + lambda_http::run(service_fn(|event: Request| handler(event))).await?; + Ok(()) +} + +pub async fn execute(event: Request) -> Result { + let name = event.query_string_parameters() + .first("name") + .unwrap_or_else(|| "stranger"); + + /// Represents an HTTP response + let response = Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(json!({ + "message": format!("Hello, {}!", name), + }).to_string()) + .unwrap(); + + Ok(response) +} +``` + +### Lambda Request Authorizer + +Because **`lambda-http`** is an abstraction, we cannot use it for the Lambda Request Authorizer case. +If you remove the abstraction, you need to handle the request/response for your service. + + +```rust +use aws_lambda_events::apigw::{ + ApiGatewayCustomAuthorizerRequestTypeRequest, ApiGatewayCustomAuthorizerResponse, +}; +use lambda_runtime::{self, service_fn, Error, LambdaEvent}; + +#[tokio::main] +async fn main() -> Result<(), Error> { + tracing_subscriber::fmt() + .with_ansi(false) + .without_time() + .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + .init(); + + lambda_http::run(service_fn(|event: Request| handler(event))).await?; + Ok(()) +} + +pub async fn execute(event: LambdaEvent) -> Result { + // do something with the event payload + let method_arn = event.payload.method_arn.unwrap(); + // for example we could het the header authorization + if let Some(token) = event.payload.headers.get("authorization") { + // do something + + return Ok(custom_authorizer_response( + "ALLOW", + "some_principal", + &method_arn, + )); + } + + Ok(custom_authorizer_response( + "DENY".to_string(), + "", + &method_arn)) +} + +pub fn custom_authorizer_response(effect: &str, principal: &str, method_arn: &str) -> ApiGatewayCustomAuthorizerResponse { + let stmt = IamPolicyStatement { + action: vec!["execute-api:Invoke".to_string()], + resource: vec![method_arn.to_owned()], + effect: Some(effect.to_owned()), + }; + let policy = ApiGatewayCustomAuthorizerPolicy { + version: Some("2012-10-17".to_string()), + statement: vec![stmt], + }; + ApiGatewayCustomAuthorizerResponse { + principal_id: Some(principal.to_owned()), + policy_document: policy, + // context: json!({ "email": principal }), https://github.com/awslabs/aws-lambda-rust-runtime/discussions/548 + usage_identifier_key: None, + } +} +``` \ No newline at end of file From 63eb33fc8ba2b3b8f6dc55db1d7d323a544339d9 Mon Sep 17 00:00:00 2001 From: Daniele <> Date: Sun, 23 Oct 2022 08:19:20 +0200 Subject: [PATCH 2/6] fix location of Readme in the cargo.toml file https://github.com/awslabs/aws-lambda-rust-runtime/pull/554#pullrequestreview-1152118326 --- lambda-http/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda-http/Cargo.toml b/lambda-http/Cargo.toml index a553f871..51d7194c 100644 --- a/lambda-http/Cargo.toml +++ b/lambda-http/Cargo.toml @@ -13,7 +13,7 @@ homepage = "https://github.com/awslabs/aws-lambda-rust-runtime" repository = "https://github.com/awslabs/aws-lambda-rust-runtime" documentation = "https://docs.rs/lambda_runtime" categories = ["web-programming::http-server"] -readme = "../README.md" +readme = "README.md" [features] default = ["apigw_rest", "apigw_http", "apigw_websockets", "alb"] From 9bff862cd0cd82b0fdbee2840b663a5c9379f7f2 Mon Sep 17 00:00:00 2001 From: Daniele <> Date: Sun, 23 Oct 2022 08:29:35 +0200 Subject: [PATCH 3/6] amend example based on the comments --- lambda-http/README.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/lambda-http/README.md b/lambda-http/README.md index 19533fa2..6974f74b 100644 --- a/lambda-http/README.md +++ b/lambda-http/README.md @@ -9,7 +9,7 @@ lambda-http handler is made of: * IntoResponse - Future that will convert an [`IntoResponse`] into an actual [`LambdaResponse`] We are able to handle requests from: -* [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) REST and HTTP API lambda integrations +* [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) REST, HTTP and WebSockets API lambda integrations * AWS [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) Thanks to the Request type we can seemsly handle proxy integrations without the worry to specify the specific service type. @@ -47,11 +47,10 @@ async fn main() -> Result<(), Error> { .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) .init(); - lambda_http::run(service_fn(|event: Request| handler(event))).await?; - Ok(()) + run(service_fn(function_handler)).await } -pub async fn execute(event: Request) -> Result { +pub async fn function_handler(event: Request) -> Result { let body = event.payload::()?; let response = Response::builder() @@ -61,7 +60,7 @@ pub async fn execute(event: Request) -> Result { "message": "Hello World", "payload": body, }).to_string()) - .unwrap(); + .map_err(Box::new)?; Ok(response) } @@ -86,11 +85,10 @@ async fn main() -> Result<(), Error> { .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) .init(); - lambda_http::run(service_fn(|event: Request| handler(event))).await?; - Ok(()) + run(service_fn(function_handler)).await } -pub async fn execute(event: Request) -> Result { +pub async fn function_handler(event: Request) -> Result { let name = event.query_string_parameters() .first("name") .unwrap_or_else(|| "stranger"); @@ -102,7 +100,7 @@ pub async fn execute(event: Request) -> Result { .body(json!({ "message": format!("Hello, {}!", name), }).to_string()) - .unwrap(); + .map_err(Box::new)?; Ok(response) } @@ -128,13 +126,13 @@ async fn main() -> Result<(), Error> { .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) .init(); - lambda_http::run(service_fn(|event: Request| handler(event))).await?; - Ok(()) + run(service_fn(function_handler)).await } -pub async fn execute(event: LambdaEvent) -> Result { +pub async fn function_handler(event: LambdaEvent) -> Result { // do something with the event payload - let method_arn = event.payload.method_arn.unwrap(); + let method_arn = event.payload.method_arn + .map_err(Box::new)?; // for example we could het the header authorization if let Some(token) = event.payload.headers.get("authorization") { // do something From 43316131a1d41e923dad0773757395224d820631 Mon Sep 17 00:00:00 2001 From: Daniele <> Date: Sun, 23 Oct 2022 09:20:02 +0200 Subject: [PATCH 4/6] make the code example to compile --- lambda-http/README.md | 81 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/lambda-http/README.md b/lambda-http/README.md index 6974f74b..c8a79886 100644 --- a/lambda-http/README.md +++ b/lambda-http/README.md @@ -29,13 +29,15 @@ Here you will find a few examples to handle basic scenarions: * Reading a JSON from a body and deserialise into a structure * Reading querystring parameters * Lambda Request Authorizer +* Passing the Lambda execution context initialisation to the handler ### Reading a JSON from a body and deserialise into a structure The code below creates a simple API Gateway proxy (HTTP, REST) that accept in input a JSON payload. ```rust -use lambda_http::{http::StatusCode, service_fn, Error, IntoResponse, Request}; +use http::Response; +use lambda_http::{run, http::StatusCode, service_fn, Error, IntoResponse, Request, RequestExt}; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -75,7 +77,9 @@ pub struct MyPayload { ### Reading querystring parameters ```rust -use lambda_http::{http::StatusCode, service_fn, Error, IntoResponse, Request, RequestExt}; +use http::Response; +use lambda_http::{run, http::StatusCode, service_fn, Error, IntoResponse, Request, RequestExt}; +use serde_json::json; #[tokio::main] async fn main() -> Result<(), Error> { @@ -91,9 +95,10 @@ async fn main() -> Result<(), Error> { pub async fn function_handler(event: Request) -> Result { let name = event.query_string_parameters() .first("name") - .unwrap_or_else(|| "stranger"); + .unwrap_or_else(|| "stranger") + .to_string(); - /// Represents an HTTP response + // Represents an HTTP response let response = Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") @@ -114,9 +119,10 @@ If you remove the abstraction, you need to handle the request/response for your ```rust use aws_lambda_events::apigw::{ - ApiGatewayCustomAuthorizerRequestTypeRequest, ApiGatewayCustomAuthorizerResponse, + ApiGatewayCustomAuthorizerRequestTypeRequest, ApiGatewayCustomAuthorizerResponse, ApiGatewayCustomAuthorizerPolicy, IamPolicyStatement, }; -use lambda_runtime::{self, service_fn, Error, LambdaEvent}; +use lambda_runtime::{run, service_fn, Error, LambdaEvent}; +use serde_json::json; #[tokio::main] async fn main() -> Result<(), Error> { @@ -131,8 +137,7 @@ async fn main() -> Result<(), Error> { pub async fn function_handler(event: LambdaEvent) -> Result { // do something with the event payload - let method_arn = event.payload.method_arn - .map_err(Box::new)?; + let method_arn = event.payload.method_arn.unwrap(); // for example we could het the header authorization if let Some(token) = event.payload.headers.get("authorization") { // do something @@ -145,7 +150,7 @@ pub async fn function_handler(event: LambdaEvent Result<(), Error> { + tracing_subscriber::fmt() + .with_ansi(false) + .without_time() + .with_max_level(tracing_subscriber::filter::LevelFilter::INFO) + .init(); + + let config = aws_config::from_env() + .load() + .await; + + let dynamodb_client = aws_sdk_dynamodb::Client::new(&config); + + run(service_fn(|event: Request| function_handler(&dynamodb_client, event))).await +} + +pub async fn function_handler(dynamodb_client: &aws_sdk_dynamodb::Client, event: Request) -> Result { + let table = std::env::var("TABLE_NAME").expect("TABLE_NAME must be set"); + + let name = event.query_string_parameters() + .first("name") + .unwrap_or_else(|| "stranger"); + + let result = client + .put_item() + .table_name(table) + .item("ID", AttributeValue::S(Utc::now().timestamp().to_string())) + .item("name", AttributeValue::S(name.into())) + .send() + .await?; + + // Represents an HTTP response + let response = Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "application/json") + .body(json!({ + "message": format!("Hello, {}!", name), + }).to_string()) + .map_err(Box::new)?; + + Ok(response) +} ``` \ No newline at end of file From 00861d2b1972b346ab805bbba84f3f86622819ee Mon Sep 17 00:00:00 2001 From: Daniele <> Date: Sun, 23 Oct 2022 10:57:11 +0200 Subject: [PATCH 5/6] amend example --- lambda-http/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lambda-http/README.md b/lambda-http/README.md index c8a79886..a1843322 100644 --- a/lambda-http/README.md +++ b/lambda-http/README.md @@ -207,13 +207,14 @@ pub async fn function_handler(dynamodb_client: &aws_sdk_dynamodb::Client, event: let name = event.query_string_parameters() .first("name") - .unwrap_or_else(|| "stranger"); + .unwrap_or_else(|| "stranger") + .to_string(); - let result = client + dynamodb_client .put_item() .table_name(table) .item("ID", AttributeValue::S(Utc::now().timestamp().to_string())) - .item("name", AttributeValue::S(name.into())) + .item("name", AttributeValue::S(name.to_owned())) .send() .await?; From a39ea5ca803a910e8d35f203284b3c030c5559aa Mon Sep 17 00:00:00 2001 From: Daniele <> Date: Mon, 24 Oct 2022 08:02:04 +0200 Subject: [PATCH 6/6] add reference for Lambda function URLs --- lambda-http/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lambda-http/README.md b/lambda-http/README.md index a1843322..20e670eb 100644 --- a/lambda-http/README.md +++ b/lambda-http/README.md @@ -11,6 +11,7 @@ lambda-http handler is made of: We are able to handle requests from: * [API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html) REST, HTTP and WebSockets API lambda integrations * AWS [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) +* AWS [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) Thanks to the Request type we can seemsly handle proxy integrations without the worry to specify the specific service type. @@ -138,7 +139,7 @@ async fn main() -> Result<(), Error> { pub async fn function_handler(event: LambdaEvent) -> Result { // do something with the event payload let method_arn = event.payload.method_arn.unwrap(); - // for example we could het the header authorization + // for example we could use the authorization header if let Some(token) = event.payload.headers.get("authorization") { // do something