From aa51114ac198327650734cd2d0821f19231206d4 Mon Sep 17 00:00:00 2001 From: Nanook Date: Fri, 21 Aug 2026 11:20:38 +0000 Subject: [PATCH 1/4] feat: enable VPC Lattice support --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed6302ce..21ff0b76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,9 +262,9 @@ dependencies = [ [[package]] name = "aws_lambda_events" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087b1b9233c7fc56623d72bb2f0b1fe915b19a0606aa3c09bcd1b902d9803e6c" +checksum = "d02c123e89527e7b424f74f52d11be0d17ef2887819323a42dcae1c7630ca53d" dependencies = [ "base64", "bytes", @@ -1245,9 +1245,9 @@ dependencies = [ [[package]] name = "lambda_http" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0f0300091919bd7c3d953bd0fb6a7a170d24f333486e4129421c0f6aa1164ac" +checksum = "7b5fc0b7ef9af5e63e348c4b11e7e31eab573603348b7eb9c3e831d8e87c1ecc" dependencies = [ "aws_lambda_events", "bytes", @@ -1270,9 +1270,9 @@ dependencies = [ [[package]] name = "lambda_runtime" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23583c918bee8de7bc005ba2ad8ebd84529894bfd9a863cf24226bb6c7787690" +checksum = "5cc1b380b7fb92ddf3913b01f094c3fcf59e31d995bb609645cd64dcdb56ce63" dependencies = [ "async-stream", "base64", @@ -1295,9 +1295,9 @@ dependencies = [ [[package]] name = "lambda_runtime_api_client" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4873061514cb57ffb6a599b77c46c65d6d783efe9bad8fd56b7cba7f0459ef" +checksum = "b5e9ffa99f1e87b21d42ac98fe7eac55f4cfb9ed14677a64a7dab4d8f44399a4" dependencies = [ "bytes", "futures-channel", diff --git a/Cargo.toml b/Cargo.toml index c9a1ff49..7d100843 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,10 +23,11 @@ http-body = "1.0.1" http-body-util = "0.1.0" hyper = { version = "1.5.2", features = ["client"] } hyper-util = "0.1.10" -lambda_http = { version = "1.1.1", default-features = false, features = [ +lambda_http = { version = "1.2.0", default-features = false, features = [ "apigw_http", "apigw_rest", "alb", + "vpc_lattice", "pass_through", "tracing", "concurrency-tokio" From fe185565a7617e7f9d8e4f28c7901778faf54ae6 Mon Sep 17 00:00:00 2001 From: Nanook Date: Fri, 21 Aug 2026 19:50:31 +0000 Subject: [PATCH 2/4] test: cover VPC Lattice and pass-through events --- tests/integ_tests/main.rs | 203 +++++++++++++++++++++++++++++++++++++- 1 file changed, 201 insertions(+), 2 deletions(-) diff --git a/tests/integ_tests/main.rs b/tests/integ_tests/main.rs index 0238618e..ed2716c7 100644 --- a/tests/integ_tests/main.rs +++ b/tests/integ_tests/main.rs @@ -14,8 +14,8 @@ use httpmock::{ MockServer, }; use hyper::body::Incoming; -use lambda_http::Body; -use lambda_http::Context; +use lambda_http::request::RequestContext; +use lambda_http::{Body, Context, RequestExt}; use lambda_web_adapter::{Adapter, AdapterOptions, LambdaInvokeMode, Protocol}; use tower::{Service, ServiceBuilder}; @@ -659,6 +659,122 @@ async fn test_http_context_headers() { assert_eq!("OK", body_to_string(response).await); } +#[tokio::test] +async fn test_non_http_event_routes_to_configured_pass_through_path() { + let app_server = MockServer::start(); + let event = pass_through_bedrock_agent_event(); + let expected_body = event.clone(); + + let endpoint = app_server.mock(move |when, then| { + when.method(POST) + .path("/lambda-events") + .header("content-type", "application/json") + .body(expected_body); + then.status(200).body("pass-through"); + }); + + let mut adapter = Adapter::new(&AdapterOptions { + host: app_server.host(), + port: app_server.port().to_string(), + readiness_check_port: app_server.port().to_string(), + readiness_check_path: "/healthcheck".to_string(), + pass_through_path: "/lambda-events".to_string(), + ..Default::default() + }) + .expect("Failed to create adapter"); + let mut request = lambda_http::request::from_str(&event).expect("Failed to deserialize event"); + + assert!(matches!(request.request_context(), RequestContext::PassThrough)); + add_lambda_context_to_request(&mut request); + + let response = adapter.call(request).await.expect("Request failed"); + + endpoint.assert(); + assert_eq!(200, response.status()); + assert_eq!("pass-through", body_to_string(response).await); +} + +#[tokio::test] +async fn test_vpc_lattice_v2_event_routes_with_path_query_and_context() { + let app_server = MockServer::start(); + let event = vpc_lattice_v2_event(); + + let expected_request_context = json!({ + "serviceNetworkArn": VPC_LATTICE_SERVICE_NETWORK_ARN, + "serviceArn": VPC_LATTICE_SERVICE_ARN, + "targetGroupArn": VPC_LATTICE_TARGET_GROUP_ARN, + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/service-role/HealthChecker", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + }); + + let endpoint = app_server.mock(move |when, then| { + when.method(POST) + .path("/health") + .query_param("state", "prod") + .query_param("mode", "fast") + .query_param("mode", "turbo") + .body(VPC_LATTICE_BODY) + .is_true(move |req| { + let mut mode_values: Vec<_> = req + .query_params() + .into_iter() + .filter(|(key, _)| key == "mode") + .map(|(_, value)| value) + .collect(); + mode_values.sort_unstable(); + if mode_values != vec!["fast".to_string(), "turbo".to_string()] { + return false; + } + + let headers = req.headers(); + let Some(request_context) = headers + .get("x-amzn-request-context") + .and_then(|value| value.to_str().ok()) + else { + return false; + }; + + let Ok(request_context) = serde_json::from_str::(request_context) else { + return false; + }; + + request_context == expected_request_context + }); + then.status(200).body("vpc lattice"); + }); + + let mut adapter = Adapter::new(&AdapterOptions { + host: app_server.host(), + port: app_server.port().to_string(), + readiness_check_port: app_server.port().to_string(), + readiness_check_path: "/healthcheck".to_string(), + ..Default::default() + }) + .expect("Failed to create adapter"); + + let mut request = lambda_http::request::from_str(&event).expect("Failed to deserialize VPC Lattice event"); + + match request.request_context() { + RequestContext::VpcLattice(context) => { + assert_eq!(VPC_LATTICE_TARGET_GROUP_ARN, context.target_group_arn); + } + other => panic!("unexpected request context: {other:?}"), + } + + add_lambda_context_to_request(&mut request); + let response = adapter.call(request).await.expect("Request failed"); + + endpoint.assert(); + assert_eq!(200, response.status()); + assert_eq!("vpc lattice", body_to_string(response).await); +} + #[tokio::test] async fn test_http_content_encoding_suffix() { // Start app server @@ -1201,6 +1317,89 @@ fn add_lambda_context_to_request(request: &mut Request) { request.extensions_mut().insert(context); } +fn pass_through_bedrock_agent_event() -> String { + json!({ + "messageVersion": "1.0", + "agent": { + "name": "AgentName", + "id": "AgentID", + "alias": "AgentAlias", + "version": "AgentVersion" + }, + "inputText": "InputText", + "sessionId": "SessionID", + "actionGroup": "ActionGroup", + "apiPath": "/api/path", + "httpMethod": "POST", + "parameters": [ + { + "name": "param1", + "type": "string", + "value": "value1" + } + ], + "requestBody": { + "content": { + "application/json": { + "properties": [ + { + "name": "prop1", + "type": "string", + "value": "value1" + } + ] + } + } + }, + "sessionAttributes": { + "attr1": "value1" + }, + "promptSessionAttributes": { + "promptAttr1": "value1" + } + }) + .to_string() +} + +const VPC_LATTICE_SERVICE_NETWORK_ARN: &str = + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a"; +const VPC_LATTICE_SERVICE_ARN: &str = "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c"; +const VPC_LATTICE_TARGET_GROUP_ARN: &str = + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09"; +const VPC_LATTICE_BODY: &str = r#"{"message":"hello from vpc lattice"}"#; + +fn vpc_lattice_v2_event() -> String { + json!({ + "version": "2.0", + "path": "/health", + "method": "POST", + "headers": { + "accept": ["*/*"], + "user-agent": ["curl/7.68.0"] + }, + "queryStringParameters": { + "state": ["prod"], + "mode": ["fast", "turbo"] + }, + "body": VPC_LATTICE_BODY, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": VPC_LATTICE_SERVICE_NETWORK_ARN, + "serviceArn": VPC_LATTICE_SERVICE_ARN, + "targetGroupArn": VPC_LATTICE_TARGET_GROUP_ARN, + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/service-role/HealthChecker", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } + }) + .to_string() +} + #[tokio::test] async fn test_concurrent_request_forwarding() { let app_server = MockServer::start(); From 8e087af55f36e621b499ae03471035b6a2148840 Mon Sep 17 00:00:00 2001 From: Nanook Date: Sat, 22 Aug 2026 01:16:33 +0000 Subject: [PATCH 3/4] test: preserve HTTP event classification --- tests/integ_tests/main.rs | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/integ_tests/main.rs b/tests/integ_tests/main.rs index ed2716c7..4f7a7074 100644 --- a/tests/integ_tests/main.rs +++ b/tests/integ_tests/main.rs @@ -694,6 +694,49 @@ async fn test_non_http_event_routes_to_configured_pass_through_path() { assert_eq!("pass-through", body_to_string(response).await); } +#[test] +fn test_http_event_request_context_classification() { + let alb_event = json!({ + "httpMethod": "GET", + "path": "/health", + "headers": {"host": "example.com"}, + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/example/abcdef" + } + }, + "isBase64Encoded": false + }) + .to_string(); + let alb_request = lambda_http::request::from_str(&alb_event).expect("Failed to deserialize ALB event"); + assert!(matches!(alb_request.request_context(), RequestContext::Alb(_))); + + let api_gateway_v2_event = json!({ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/health", + "requestContext": { + "requestId": "abcdef", + "stage": "$default", + "http": { + "method": "GET", + "path": "/health", + "protocol": "HTTP/1.1", + "sourceIp": "127.0.0.1", + "userAgent": "curl/8.0.0" + } + }, + "isBase64Encoded": false + }) + .to_string(); + let api_gateway_v2_request = + lambda_http::request::from_str(&api_gateway_v2_event).expect("Failed to deserialize API Gateway V2 event"); + assert!(matches!( + api_gateway_v2_request.request_context(), + RequestContext::ApiGatewayV2(_) + )); +} + #[tokio::test] async fn test_vpc_lattice_v2_event_routes_with_path_query_and_context() { let app_server = MockServer::start(); From 74b5c44abd8aca9ca068a681589693aa80cdb220 Mon Sep 17 00:00:00 2001 From: Nanook Date: Mon, 24 Aug 2026 00:11:00 +0000 Subject: [PATCH 4/4] test: cover REST and pass-through classification --- README.md | 1 + docs/guide/src/features/request-context.md | 4 +++- tests/integ_tests/main.rs | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c585c905..12a83e3b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ The same docker image can run on AWS Lambda, Amazon EC2, AWS Fargate, and local - Run web applications on AWS Lambda - Supports Amazon API Gateway Rest API and Http API endpoints, Lambda Function URLs, and Application Load Balancer +- Supports VPC Lattice V2 events with target-group configuration - Supports Lambda managed runtimes, custom runtimes and docker OCI images - Supports Lambda Managed Instances for multi-concurrent request handling - Supports any web frameworks and languages, no new code dependency to include diff --git a/docs/guide/src/features/request-context.md b/docs/guide/src/features/request-context.md index 3cee18af..93b5dbeb 100644 --- a/docs/guide/src/features/request-context.md +++ b/docs/guide/src/features/request-context.md @@ -4,7 +4,7 @@ Lambda Web Adapter forwards API Gateway request context and Lambda invocation co ## Request Context -API Gateway sends metadata (requestId, requestTime, apiId, identity, authorizer) for each request. This is forwarded in the `x-amzn-request-context` header as a JSON string. +API Gateway sends metadata (requestId, requestTime, apiId, identity, authorizer) for each request. VPC Lattice also sends request context metadata. These contexts are forwarded in the `x-amzn-request-context` header as a JSON string. The identity and authorizer fields are particularly useful for client authorization. @@ -19,6 +19,8 @@ app.get('/', (req, res) => { See the [API Gateway docs](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format) for the full request context schema. +For VPC Lattice, the adapter supports the V2 payload format and requires target-group configuration. + ## Lambda Context The Lambda invocation context (function name, memory, timeout, request ID, etc.) is forwarded in the `x-amzn-lambda-context` header as a JSON string. diff --git a/tests/integ_tests/main.rs b/tests/integ_tests/main.rs index 4f7a7074..5d80770e 100644 --- a/tests/integ_tests/main.rs +++ b/tests/integ_tests/main.rs @@ -696,6 +696,27 @@ async fn test_non_http_event_routes_to_configured_pass_through_path() { #[test] fn test_http_event_request_context_classification() { + let sqs_event = include_str!("../../examples/sqs-expressjs/events/sqs.json"); + let sqs_request = lambda_http::request::from_str(sqs_event).expect("Failed to deserialize SQS event"); + assert!(matches!(sqs_request.request_context(), RequestContext::PassThrough)); + + let api_gateway_v1_event = json!({ + "httpMethod": "GET", + "path": "/health", + "requestContext": { + "requestId": "abcdef", + "stage": "prod", + "httpMethod": "GET" + } + }) + .to_string(); + let api_gateway_v1_request = + lambda_http::request::from_str(&api_gateway_v1_event).expect("Failed to deserialize API Gateway V1 event"); + assert!(matches!( + api_gateway_v1_request.request_context(), + RequestContext::ApiGatewayV1(_) + )); + let alb_event = json!({ "httpMethod": "GET", "path": "/health",