From 7d1a36d82807f8e65fe879ed03f7b46dec99e5f5 Mon Sep 17 00:00:00 2001 From: Nanook Date: Fri, 21 Aug 2026 11:20:38 +0000 Subject: [PATCH 1/6] feat: enable VPC Lattice support --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 54716260..0f1d93c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ lambda_http = { version = "1.3.0", default-features = false, features = [ "apigw_http", "apigw_rest", "alb", + "vpc_lattice", "pass_through", "tracing", "concurrency-tokio" From a57e17f76d78ce9479f960336a594ca85351c04f Mon Sep 17 00:00:00 2001 From: Nanook Date: Fri, 21 Aug 2026 19:50:31 +0000 Subject: [PATCH 2/6] test: cover VPC Lattice and pass-through events --- tests/integ_tests/main.rs | 204 +++++++++++++++++++++++++++++++++++++- 1 file changed, 202 insertions(+), 2 deletions(-) diff --git a/tests/integ_tests/main.rs b/tests/integ_tests/main.rs index ba0f1c5d..e056f419 100644 --- a/tests/integ_tests/main.rs +++ b/tests/integ_tests/main.rs @@ -13,8 +13,9 @@ use httpmock::{ Method::{DELETE, GET, POST, PUT}, MockServer, }; -use lambda_http::Body; -use lambda_http::Context; +use hyper::body::Incoming; +use lambda_http::request::RequestContext; +use lambda_http::{Body, Context, RequestExt}; use lambda_web_adapter::{Adapter, AdapterOptions, LambdaInvokeMode, Protocol}; use tower::{Service, ServiceBuilder}; @@ -661,6 +662,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 @@ -1217,6 +1334,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 6051df4906b582121ba029a31d4d599ad2821876 Mon Sep 17 00:00:00 2001 From: Nanook Date: Sat, 22 Aug 2026 01:16:33 +0000 Subject: [PATCH 3/6] 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 e056f419..86bf8b51 100644 --- a/tests/integ_tests/main.rs +++ b/tests/integ_tests/main.rs @@ -697,6 +697,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 d8293616ec82d1e2c71c1aefe5dfdac3b18ced27 Mon Sep 17 00:00:00 2001 From: Nanook Date: Mon, 24 Aug 2026 00:11:00 +0000 Subject: [PATCH 4/6] 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 67c2f2fb..d9b36d36 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 Lambda SnapStart with before-checkpoint and after-restore hooks 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 86bf8b51..54454b86 100644 --- a/tests/integ_tests/main.rs +++ b/tests/integ_tests/main.rs @@ -699,6 +699,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", From 145a3d356b15d9e1cd7a52c246c3b499a20c9c13 Mon Sep 17 00:00:00 2001 From: Nanook Claw Date: Wed, 16 Sep 2026 17:19:11 +0000 Subject: [PATCH 5/6] test: relax VPC Lattice request matcher Accept the request-context fields that the adapter must preserve while allowing additional context fields and JSON formatting differences. --- tests/integ_tests/main.rs | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/integ_tests/main.rs b/tests/integ_tests/main.rs index 54454b86..e154281d 100644 --- a/tests/integ_tests/main.rs +++ b/tests/integ_tests/main.rs @@ -13,7 +13,6 @@ use httpmock::{ Method::{DELETE, GET, POST, PUT}, MockServer, }; -use hyper::body::Incoming; use lambda_http::request::RequestContext; use lambda_http::{Body, Context, RequestExt}; use lambda_web_adapter::{Adapter, AdapterOptions, LambdaInvokeMode, Protocol}; @@ -784,21 +783,10 @@ async fn test_vpc_lattice_v2_event_routes_with_path_query_and_context() { when.method(POST) .path("/health") .query_param("state", "prod") - .query_param("mode", "fast") - .query_param("mode", "turbo") - .body(VPC_LATTICE_BODY) + .query_param_count("mode", "fast", 1) + .query_param_count("mode", "turbo", 1) + .json_body(serde_json::from_str::(VPC_LATTICE_BODY).expect("valid JSON 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") @@ -811,7 +799,18 @@ async fn test_vpc_lattice_v2_event_routes_with_path_query_and_context() { return false; }; - request_context == expected_request_context + expected_request_context + .as_object() + .into_iter() + .flatten() + .all(|(key, expected)| match (key.as_str(), expected) { + ("identity", expected_identity) => expected_identity + .as_object() + .into_iter() + .flatten() + .all(|(key, expected)| request_context["identity"][key] == *expected), + (key, expected) => request_context[key] == *expected, + }) }); then.status(200).body("vpc lattice"); }); From 19156ae34e373690ca071d8f63b14325d2999cfb Mon Sep 17 00:00:00 2001 From: Nanook Claw Date: Mon, 28 Sep 2026 15:15:33 +0000 Subject: [PATCH 6/6] docs: clarify VPC Lattice event versions --- README.md | 2 +- docs/guide/src/features/request-context.md | 4 ++-- docs/guide/src/introduction.md | 4 ++-- docs/guide/src/reference/architecture.md | 3 ++- tests/integ_tests/main.rs | 14 +++----------- 5 files changed, 10 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d9b36d36..1d8d1d80 100644 --- a/README.md +++ b/README.md @@ -13,7 +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 VPC Lattice Lambda event structure version V2 with target-group configuration; V1 events are treated as non-HTTP events and passed through to `AWS_LWA_PASS_THROUGH_PATH` (default `/events`) - Supports Lambda managed runtimes, custom runtimes and docker OCI images - Supports Lambda Managed Instances for multi-concurrent request handling - Supports Lambda SnapStart with before-checkpoint and after-restore hooks diff --git a/docs/guide/src/features/request-context.md b/docs/guide/src/features/request-context.md index 93b5dbeb..a171f2dc 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. VPC Lattice also sends request context metadata. These contexts are 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 event structure version V2 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,7 +19,7 @@ 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. +For VPC Lattice, set the target group's Lambda event structure version to V2. The adapter supports the V2 payload format and forwards its request context. V1 events are treated as non-HTTP events and passed through to `AWS_LWA_PASS_THROUGH_PATH`, which defaults to `/events`. ## Lambda Context diff --git a/docs/guide/src/introduction.md b/docs/guide/src/introduction.md index a8feb842..328aa90d 100644 --- a/docs/guide/src/introduction.md +++ b/docs/guide/src/introduction.md @@ -8,7 +8,7 @@ AWS Lambda Web Adapter lets developers build web apps with familiar frameworks ( ## Key Features -- Supports Amazon API Gateway (REST & HTTP API), Lambda Function URLs, and Application Load Balancer +- Supports Amazon API Gateway (REST & HTTP API), VPC Lattice Lambda event structure version V2, Lambda Function URLs, and Application Load Balancer - Works with Lambda managed runtimes, custom runtimes, and Docker OCI images - Supports Lambda Managed Instances for multi-concurrent request handling - Framework and language agnostic — no new code dependencies @@ -17,7 +17,7 @@ AWS Lambda Web Adapter lets developers build web apps with familiar frameworks ( - Response payload compression (gzip/brotli) - Response streaming - Multi-tenancy via tenant ID propagation -- Non-HTTP event trigger support (SQS, SNS, S3, DynamoDB, Kinesis, Kafka, EventBridge, Bedrock Agents) +- Non-HTTP event trigger support (SQS, SNS, S3, DynamoDB, Kinesis, Kafka, EventBridge, Bedrock Agents, and VPC Lattice event structure version V1) via pass-through ## How It Works diff --git a/docs/guide/src/reference/architecture.md b/docs/guide/src/reference/architecture.md index a3ca3e02..94360cc5 100644 --- a/docs/guide/src/reference/architecture.md +++ b/docs/guide/src/reference/architecture.md @@ -8,9 +8,10 @@ AWS Lambda Web Adapter is a Lambda Extension that bridges the gap between Lambda - Amazon API Gateway REST API - Amazon API Gateway HTTP API (v2 event format) +- VPC Lattice (Lambda event structure version V2) - Application Load Balancer (ALB) - Lambda Function URLs -- Non-HTTP triggers (SQS, SNS, S3, DynamoDB, Kinesis, Kafka, EventBridge, Bedrock Agents) via pass-through +- Non-HTTP triggers (SQS, SNS, S3, DynamoDB, Kinesis, Kafka, EventBridge, Bedrock Agents, and VPC Lattice event structure version V1) via pass-through ## Request Flow diff --git a/tests/integ_tests/main.rs b/tests/integ_tests/main.rs index e154281d..671f3fba 100644 --- a/tests/integ_tests/main.rs +++ b/tests/integ_tests/main.rs @@ -702,16 +702,7 @@ fn test_http_event_request_context_classification() { 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_event = include_str!("../../examples/fastapi/events/event.json"); let api_gateway_v1_request = lambda_http::request::from_str(&api_gateway_v1_event).expect("Failed to deserialize API Gateway V1 event"); assert!(matches!( @@ -722,7 +713,8 @@ fn test_http_event_request_context_classification() { let alb_event = json!({ "httpMethod": "GET", "path": "/health", - "headers": {"host": "example.com"}, + "multiValueHeaders": {"host": ["example.com"]}, + "multiValueQueryStringParameters": {"state": ["prod"]}, "requestContext": { "elb": { "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/example/abcdef"