diff --git a/.gitignore b/.gitignore index 51840b6d..d8feb244 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ output.json .aws-sam build +.vscode diff --git a/README.md b/README.md index f49ebd1e..46f87e5c 100644 --- a/README.md +++ b/README.md @@ -330,12 +330,6 @@ You can read more about how [cargo lambda start](https://github.com/calavera/car Lambdas can be run and debugged locally using a special [Lambda debug proxy](https://github.com/rimutaka/lambda-debug-proxy) (a non-AWS repo maintained by @rimutaka), which is a Lambda function that forwards incoming requests to one AWS SQS queue and reads responses from another queue. A local proxy running on your development computer reads the queue, calls your Lambda locally and sends back the response. This approach allows debugging of Lambda functions locally while being part of your AWS workflow. The Lambda handler code does not need to be modified between the local and AWS versions. -## `lambda_runtime` - -`lambda_runtime` is a library for authoring reliable and performant Rust-based AWS Lambda functions. At a high level, it provides `lambda_runtime::run`, a function that runs a `tower::Service`. - -To write a function that will handle request, you need to pass it through `service_fn`, which will convert your function into a `tower::Service`, which can then be run by `lambda_runtime::run`. - ## AWS event objects This project does not currently include Lambda event struct definitions. Instead, the community-maintained [`aws_lambda_events`](https://crates.io/crates/aws_lambda_events) crate can be leveraged to provide strongly-typed Lambda event structs. You can create your own custom event objects and their corresponding structs as well. @@ -378,6 +372,30 @@ fn main() -> Result<(), Box> { } ``` +## Feature flags in lambda_http + +`lambda_http` is a wrapper for HTTP events coming from three different services, Amazon Load Balancer (ALB), Amazon Api Gateway (APIGW), and AWS Lambda Function URLs. Amazon Api Gateway can also send events from three different endpoints, REST APIs, HTTP APIs, and WebSockets. `lambda_http` transforms events from all these sources into native `http::Request` objects, so you can incorporate Rust HTTP semantics into your Lambda functions. + +By default, `lambda_http` compiles your function to support any of those services. This increases the compile time of your function because we have to generate code for all the sources. In reality, you'll usually put a Lambda function only behind one of those sources. You can choose which source to generate code for with feature flags. + +The available features flags for `lambda_http` are the following: + +- `alb`: for events coming from [Amazon Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/). +- `apigw_rest`: for events coming from [Amazon API Gateway Rest APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html). +- `apigw_http`: for events coming from [Amazon API Gateway HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html) and [AWS Lambda Function URLs](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html). +- `apigw_websockets`: for events coming from [Amazon API Gateway WebSockets](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html). + +If you only want to support one of these sources, you can disable the default features, and enable only the source that you care about in your package's `Cargo.toml` file. Substitute the dependency line for `lambda_http` for the snippet below, changing the feature that you want to enable: + +```toml +[dependencies.lambda_http] +version = "0.5.3" +default-features = false +features = ["apigw_rest"] +``` + +This will make your function compile much faster. + ## Supported Rust Versions (MSRV) The AWS Lambda Rust Runtime requires a minimum of Rust 1.54, and is not guaranteed to build on compiler versions earlier than that. diff --git a/lambda-http/Cargo.toml b/lambda-http/Cargo.toml index a032a131..57a43f55 100644 --- a/lambda-http/Cargo.toml +++ b/lambda-http/Cargo.toml @@ -12,12 +12,14 @@ documentation = "https://docs.rs/lambda_runtime" categories = ["web-programming::http-server"] readme = "../README.md" -[badges] -travis-ci = { repository = "awslabs/aws-lambda-rust-runtime" } -maintenance = { status = "actively-developed" } +[features] +default = ["apigw_rest", "apigw_http", "apigw_websockets", "alb"] +apigw_rest = [] +apigw_http = [] +apigw_websockets = [] +alb = [] [dependencies] -aws_lambda_events = { version = "^0.6.3", default-features = false, features = ["alb", "apigw"]} base64 = "0.13.0" bytes = "1" http = "0.2" @@ -28,6 +30,11 @@ serde_json = "^1" serde_urlencoded = "0.7.0" query_map = { version = "0.5", features = ["url-query"] } +[dependencies.aws_lambda_events] +version = "^0.6.3" +default-features = false +features = ["alb", "apigw"] + [dev-dependencies] log = "^0.4" maplit = "1.0" diff --git a/lambda-http/src/request.rs b/lambda-http/src/request.rs index d6b0678d..c6327b7e 100644 --- a/lambda-http/src/request.rs +++ b/lambda-http/src/request.rs @@ -4,11 +4,14 @@ //! request extension method provided by [lambda_http::RequestExt](../trait.RequestExt.html) //! use crate::ext::{PathParameters, QueryStringParameters, RawHttpPath, StageVariables}; +#[cfg(feature = "alb")] use aws_lambda_events::alb::{AlbTargetGroupRequest, AlbTargetGroupRequestContext}; -use aws_lambda_events::apigw::{ - ApiGatewayProxyRequest, ApiGatewayProxyRequestContext, ApiGatewayV2httpRequest, ApiGatewayV2httpRequestContext, - ApiGatewayWebsocketProxyRequest, ApiGatewayWebsocketProxyRequestContext, -}; +#[cfg(feature = "apigw_rest")] +use aws_lambda_events::apigw::{ApiGatewayProxyRequest, ApiGatewayProxyRequestContext}; +#[cfg(feature = "apigw_http")] +use aws_lambda_events::apigw::{ApiGatewayV2httpRequest, ApiGatewayV2httpRequestContext}; +#[cfg(feature = "apigw_websockets")] +use aws_lambda_events::apigw::{ApiGatewayWebsocketProxyRequest, ApiGatewayWebsocketProxyRequestContext}; use aws_lambda_events::encodings::Body; use http::header::HeaderName; use query_map::QueryMap; @@ -25,9 +28,13 @@ use std::{io::Read, mem}; #[derive(Deserialize, Debug)] #[serde(untagged)] pub enum LambdaRequest { + #[cfg(feature = "apigw_rest")] ApiGatewayV1(ApiGatewayProxyRequest), + #[cfg(feature = "apigw_http")] ApiGatewayV2(ApiGatewayV2httpRequest), + #[cfg(feature = "alb")] Alb(AlbTargetGroupRequest), + #[cfg(feature = "apigw_websockets")] WebSocket(ApiGatewayWebsocketProxyRequest), } @@ -37,9 +44,13 @@ impl LambdaRequest { /// type of response the request origin expects. pub fn request_origin(&self) -> RequestOrigin { match self { + #[cfg(feature = "apigw_rest")] LambdaRequest::ApiGatewayV1 { .. } => RequestOrigin::ApiGatewayV1, + #[cfg(feature = "apigw_http")] LambdaRequest::ApiGatewayV2 { .. } => RequestOrigin::ApiGatewayV2, + #[cfg(feature = "alb")] LambdaRequest::Alb { .. } => RequestOrigin::Alb, + #[cfg(feature = "apigw_websockets")] LambdaRequest::WebSocket { .. } => RequestOrigin::WebSocket, } } @@ -50,15 +61,20 @@ impl LambdaRequest { #[derive(Debug)] pub enum RequestOrigin { /// API Gateway request origin + #[cfg(feature = "apigw_rest")] ApiGatewayV1, /// API Gateway v2 request origin + #[cfg(feature = "apigw_http")] ApiGatewayV2, /// ALB request origin + #[cfg(feature = "alb")] Alb, /// API Gateway WebSocket + #[cfg(feature = "apigw_websockets")] WebSocket, } +#[cfg(feature = "apigw_http")] fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request { let http_method = ag.request_context.http.method.clone(); let raw_path = ag.raw_path.unwrap_or_default(); @@ -130,6 +146,7 @@ fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request http::Request { let http_method = ag.http_method; let raw_path = ag.path.unwrap_or_default(); @@ -196,6 +213,7 @@ fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request { req } +#[cfg(feature = "alb")] fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request { let http_method = alb.http_method; let raw_path = alb.path.unwrap_or_default(); @@ -261,6 +279,7 @@ fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request { req } +#[cfg(feature = "apigw_websockets")] fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request { let http_method = ag.http_method; let builder = http::Request::builder() @@ -338,12 +357,16 @@ fn apigw_path_with_stage(stage: &Option, path: &str) -> String { #[serde(untagged)] pub enum RequestContext { /// API Gateway proxy request context + #[cfg(feature = "apigw_rest")] ApiGatewayV1(ApiGatewayProxyRequestContext), /// API Gateway v2 request context + #[cfg(feature = "apigw_http")] ApiGatewayV2(ApiGatewayV2httpRequestContext), /// ALB request context + #[cfg(feature = "alb")] Alb(AlbTargetGroupRequestContext), /// WebSocket request context + #[cfg(feature = "apigw_websockets")] WebSocket(ApiGatewayWebsocketProxyRequestContext), } @@ -351,9 +374,13 @@ pub enum RequestContext { impl<'a> From for http::Request { fn from(value: LambdaRequest) -> Self { match value { - LambdaRequest::ApiGatewayV2(ag) => into_api_gateway_v2_request(ag), + #[cfg(feature = "apigw_rest")] LambdaRequest::ApiGatewayV1(ag) => into_proxy_request(ag), + #[cfg(feature = "apigw_http")] + LambdaRequest::ApiGatewayV2(ag) => into_api_gateway_v2_request(ag), + #[cfg(feature = "alb")] LambdaRequest::Alb(alb) => into_alb_request(alb), + #[cfg(feature = "apigw_websockets")] LambdaRequest::WebSocket(ag) => into_websocket_request(ag), } } @@ -422,7 +449,7 @@ mod tests { } #[test] - fn deserializes_minimal_apigw_v2_request_events() { + fn deserializes_minimal_apigw_http_request_events() { // from the docs // https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request let input = include_str!("../tests/data/apigw_v2_proxy_request_minimal.json"); @@ -450,7 +477,7 @@ mod tests { } #[test] - fn deserializes_apigw_v2_request_events() { + fn deserializes_apigw_http_request_events() { // from the docs // https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-api-gateway-request let input = include_str!("../tests/data/apigw_v2_proxy_request.json"); @@ -593,7 +620,7 @@ mod tests { } #[test] - fn deserialize_apigw_v2_sam_local() { + fn deserialize_apigw_http_sam_local() { // manually generated from AWS SAM CLI // Steps to recreate: // * sam init diff --git a/lambda-http/src/response.rs b/lambda-http/src/response.rs index 4ea9c895..0c46ae68 100644 --- a/lambda-http/src/response.rs +++ b/lambda-http/src/response.rs @@ -2,8 +2,12 @@ use crate::request::RequestOrigin; use aws_lambda_events::encodings::Body; +#[cfg(feature = "alb")] use aws_lambda_events::event::alb::AlbTargetGroupResponse; -use aws_lambda_events::event::apigw::{ApiGatewayProxyResponse, ApiGatewayV2httpResponse}; +#[cfg(any(feature = "apigw_rest", feature = "apigw_websockets"))] +use aws_lambda_events::event::apigw::ApiGatewayProxyResponse; +#[cfg(feature = "apigw_http")] +use aws_lambda_events::event::apigw::ApiGatewayV2httpResponse; use http::{ header::{CONTENT_TYPE, SET_COOKIE}, Response, @@ -15,8 +19,11 @@ use serde::Serialize; #[derive(Serialize, Debug)] #[serde(untagged)] pub enum LambdaResponse { - ApiGatewayV2(ApiGatewayV2httpResponse), + #[cfg(any(feature = "apigw_rest", feature = "apigw_websockets"))] ApiGatewayV1(ApiGatewayProxyResponse), + #[cfg(feature = "apigw_http")] + ApiGatewayV2(ApiGatewayV2httpResponse), + #[cfg(feature = "alb")] Alb(AlbTargetGroupResponse), } @@ -37,6 +44,15 @@ impl LambdaResponse { let status_code = parts.status.as_u16(); match request_origin { + #[cfg(feature = "apigw_rest")] + RequestOrigin::ApiGatewayV1 => LambdaResponse::ApiGatewayV1(ApiGatewayProxyResponse { + body, + status_code: status_code as i64, + is_base64_encoded: Some(is_base64_encoded), + headers: headers.clone(), + multi_value_headers: headers, + }), + #[cfg(feature = "apigw_http")] RequestOrigin::ApiGatewayV2 => { // ApiGatewayV2 expects the set-cookies headers to be in the "cookies" attribute, // so remove them from the headers. @@ -57,13 +73,7 @@ impl LambdaResponse { multi_value_headers: headers, }) } - RequestOrigin::ApiGatewayV1 => LambdaResponse::ApiGatewayV1(ApiGatewayProxyResponse { - body, - status_code: status_code as i64, - is_base64_encoded: Some(is_base64_encoded), - headers: headers.clone(), - multi_value_headers: headers, - }), + #[cfg(feature = "alb")] RequestOrigin::Alb => LambdaResponse::Alb(AlbTargetGroupResponse { body, status_code: status_code as i64, @@ -76,6 +86,7 @@ impl LambdaResponse { parts.status.canonical_reason().unwrap_or_default() )), }), + #[cfg(feature = "apigw_websockets")] RequestOrigin::WebSocket => LambdaResponse::ApiGatewayV1(ApiGatewayProxyResponse { body, status_code: status_code as i64,