Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature flags for lambda_http #497

Merged
merged 5 commits into from
Jul 6, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ output.json

.aws-sam
build
.vscode
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed this because it's part of an old readme, and we didn't notice until now. It doesn't make sense anymore in this context.


`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<LambdaEvent>`.

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<LambdaEvent>`, 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.
Expand Down Expand Up @@ -378,6 +372,30 @@ fn main() -> Result<(), Box<Error>> {
}
```

## 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.
calavera marked this conversation as resolved.
Show resolved Hide resolved

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.
Expand Down
15 changes: 11 additions & 4 deletions lambda-http/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
43 changes: 35 additions & 8 deletions lambda-http/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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),
}

Expand All @@ -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,
}
}
Expand All @@ -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<Body> {
let http_method = ag.request_context.http.method.clone();
let raw_path = ag.raw_path.unwrap_or_default();
Expand Down Expand Up @@ -130,6 +146,7 @@ fn into_api_gateway_v2_request(ag: ApiGatewayV2httpRequest) -> http::Request<Bod
req
}

#[cfg(feature = "apigw_rest")]
fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request<Body> {
let http_method = ag.http_method;
let raw_path = ag.path.unwrap_or_default();
Expand Down Expand Up @@ -196,6 +213,7 @@ fn into_proxy_request(ag: ApiGatewayProxyRequest) -> http::Request<Body> {
req
}

#[cfg(feature = "alb")]
fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request<Body> {
let http_method = alb.http_method;
let raw_path = alb.path.unwrap_or_default();
Expand Down Expand Up @@ -261,6 +279,7 @@ fn into_alb_request(alb: AlbTargetGroupRequest) -> http::Request<Body> {
req
}

#[cfg(feature = "apigw_websockets")]
fn into_websocket_request(ag: ApiGatewayWebsocketProxyRequest) -> http::Request<Body> {
let http_method = ag.http_method;
let builder = http::Request::builder()
Expand Down Expand Up @@ -338,22 +357,30 @@ fn apigw_path_with_stage(stage: &Option<String>, 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),
}

/// Converts LambdaRequest types into `http::Request<Body>` types
impl<'a> From<LambdaRequest> for http::Request<Body> {
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),
}
}
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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
Expand Down
29 changes: 20 additions & 9 deletions lambda-http/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
}

Expand All @@ -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.
Expand All @@ -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,
Expand All @@ -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,
Expand Down