Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ lambda_http = { version = "1.3.0", default-features = false, features = [
"apigw_http",
"apigw_rest",
"alb",
"vpc_lattice",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[BUG] Adding another HTTP event variant alongside pass_through changes event classification, and nothing in this PR verifies that non-HTTP events still behave as before.

pass_through is the fallback variant of lambda_http's event enum, and it is the adapter's only mechanism for non-HTTP triggers. In src/lib.rs:

if matches!(request_context, RequestContext::PassThrough) && parts.method == Method::POST {
   path = self.pass_through_path.as_str();
}

Every SQS, SNS, S3, DynamoDB, EventBridge, and Bedrock Agent payload reaches the app only because it deserializes into PassThrough (documented in docs/guide/src/features/non-http-events.md, exercised by examples/sqs-expressjs and examples/bedrock-agent-fastapi). Enabling one more variant necessarily shrinks the set of payloads that reach that fallback. VPC Lattice payloads are shaped as loosely typed method / raw_path / headers / query_string_parameters / body / is_base64_encoded fields, so if those fields deserialize with defaults, an unrelated event JSON can match the Lattice variant instead of falling through. The failure is silent: the event would be forwarded as a GET to / with an empty body rather than POSTed to AWS_LWA_PASS_THROUGH_PATH, so a pass-through handler would simply stop receiving messages.

Two things are worth adding before merge:

  1. A regression test asserting a non-HTTP payload still routes to the pass-through path. The current harness cannot express this — tests/integ_tests/common/mod.rs only builds ALB events:
pub enum LambdaEventType {
   #[default]
   ALB,
   // TODO: Add other event types
}
  1. A test covering the new path itself: a VPC Lattice event producing the expected request path, query string, and x-amzn-request-context header. There is currently no coverage that the newly enabled variant works end to end through fetch_response, which derives the path from raw_http_path() and serializes the context into a header.

This matters more than usual here because the PR description notes cargo check --locked could not complete in the author's environment, so neither compilation nor the existing test suite has been run against the change.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[GENERAL] Enabling vpc_lattice makes VPC Lattice a supported trigger, but no user-facing documentation in this repo reflects that, and the supported payload format is left implicit.

Concretely stale/incomplete after this change:

  • README.md — the features list still reads "Supports Amazon API Gateway Rest API and Http API endpoints, Lambda Function URLs, and Application Load Balancer".
  • docs/guide/src/features/request-context.md — describes x-amzn-request-context purely as "API Gateway request context"; with this change the header can now carry a VPC Lattice context (serviceNetworkArn, serviceArn, targetGroupArn, identity), which is exactly the metadata an app behind Lattice would read for authorization.

The payload-format point matters behaviorally, not just editorially: the PR description and the test fixture ("version": "2.0") target the VPC Lattice V2 event structure. A Lattice target group configured with the other payload format would not match that variant and would instead fall back to the pass-through path in src/lib.rs:

if matches!(request_context, RequestContext::PassThrough) && parts.method == Method::POST {
   path = self.pass_through_path.as_str();
}

That means a misconfigured target group silently POSTs the raw event to AWS_LWA_PASS_THROUGH_PATH (default /events) instead of the app's real route — a failure mode that is very hard to diagnose without a documented requirement. Please state which payload format(s) are supported and note the target-group configuration requirement.

"pass_through",
"tracing",
"concurrency-tokio"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand Down
4 changes: 3 additions & 1 deletion docs/guide/src/features/request-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.

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

The Lambda invocation context (function name, memory, timeout, request ID, etc.) is forwarded in the `x-amzn-lambda-context` header as a JSON string.
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/src/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
3 changes: 2 additions & 1 deletion docs/guide/src/reference/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
259 changes: 257 additions & 2 deletions tests/integ_tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use httpmock::{
Method::{DELETE, GET, POST, PUT},
MockServer,
};
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};

Expand Down Expand Up @@ -661,6 +661,178 @@ 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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[GENERAL] The regression guard for the classification change covers a single non-HTTP payload shape, which leaves most of the documented pass-through surface unguarded.

This PR inserts a new variant into lambda_http's untagged event enum, where pass_through is the fallback and variants are tried in declaration order. test_non_http_event_routes_to_configured_pass_through_path proves a Bedrock Agent payload still resolves to RequestContext::PassThrough, but docs/guide/src/features/non-http-events.md claims support for "SQS, SNS, S3, DynamoDB, Kinesis, Kafka, EventBridge, and Bedrock Agents". A records-style payload ({"Records": [...]}) has a completely different shape from the Bedrock payload, so it exercises a different matching path and is the more representative case for the adapter's non-HTTP triggers — and the repo already ships a fixture at examples/sqs-expressjs/events/sqs.json to model it after.

Similarly, test_http_event_request_context_classification asserts ALB and API Gateway V2 but omits API Gateway REST (V1), which is one of the adapter's headline supported triggers and is equally subject to variant-ordering changes.

Suggested additions, following the pattern already established in the new test:

let sqs_event = json!({
   "Records": [{
       "messageId": "059f36b4-87a3-44ab-83d2-661975830a7d",
       "receiptHandle": "AQEBwJnKyrHigUMZj6rYigCgxlaS3SLy0a",
       "body": "Test message.",
       "eventSource": "aws:sqs",
       "awsRegion": "us-east-1"
   }]
})
.to_string();
let sqs_request = lambda_http::request::from_str(&sqs_event).expect("Failed to deserialize SQS event");
assert!(matches!(sqs_request.request_context(), RequestContext::PassThrough));

Without these, a future variant reordering or event-struct loosening in lambda_http could silently reroute non-HTTP triggers away from AWS_LWA_PASS_THROUGH_PATH and the suite would still pass.

Cargo.lock was reviewed as a lock file only (version/checksum bumps for aws_lambda_events, lambda_http, lambda_runtime, lambda_runtime_api_client); no findings. I did not evaluate whether the vpc_lattice feature or the pinned versions resolve correctly, since the crate sources are not available in this workspace and the PR notes cargo check --locked could not complete — worth confirming in CI before merge, given the 244 lines of new test code have not been compiled.

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);
}

#[test]
fn test_http_event_request_context_classification() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[GENERAL] The classification guard misses the payload shapes most at risk from inserting a variant into an untagged enum.

Context that raises the stakes: every other integration test builds its event as a typed value (LambdaRequest::Alb in tests/integ_tests/common/mod.rs), which bypasses serde's untagged variant selection entirely. So this test plus the two new event-string tests are the only coverage for the one behavioral thing this PR changes — which payload resolves to which RequestContext.

The ALB fixture here covers only the multi-value-headers-disabled shape:

"headers": {"host": "example.com"},

An ALB target group with multi-value headers enabled sends multiValueHeaders / multiValueQueryStringParameters and omits headers / queryStringParameters — i.e. header and query maps whose values are arrays, which is precisely the shape VPC Lattice V2 uses ("headers": {"accept": ["*/*"]}, as in vpc_lattice_v2_event()). That is the shape a newly inserted variant is most likely to capture, and no fixture in this test — or anywhere in the repo, I checked examples/*/events/*.json — exercises it. The same gap applies to API Gateway REST with multi-value headers.

Two cheap improvements:

  • Add an ALB case with multiValueHeaders/multiValueQueryStringParameters and no single-value maps, asserting RequestContext::Alb(_).
  • Use the realistic REST payload the repo already ships instead of the 4-key object, the same way the SQS case is loaded:
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!(
    api_gateway_v1_request.request_context(),
    RequestContext::ApiGatewayV1(_)
));

That also removes a maintenance trap: a minimal fixture pins the variant only for the fields it happens to include, so it can keep passing while a real event with a conflicting field type classifies differently.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[GENERAL] The PR documents VPC Lattice V1 behavior in four places but never tests it, and V1 is the one classification outcome the new variant is most likely to change.

The diff adds this contract to README.md, docs/guide/src/introduction.md, docs/guide/src/reference/architecture.md, and docs/guide/src/features/request-context.md:

V1 events are treated as non-HTTP events and passed through to AWS_LWA_PASS_THROUGH_PATH

test_http_event_request_context_classification covers SQS, API Gateway V1/V2, and ALB, and the new test_vpc_lattice_v2_event_routes_with_path_query_and_context covers V2 — but nothing covers V1. That is the gap that matters most here, because a V1 payload is the shape closest to the variant this PR inserts: it carries method, headers, query_string_parameters, body, and is_base64_encoded, differing from V2 mainly in snake_case keys, raw_path instead of path, and the absence of requestContext. In lambda_http's untagged enum a variant is selected when every field it requires is satisfiable, so whether a V1 event lands on PassThrough or partially matches the new VpcLattice variant depends on which V2 fields are optional — not something the documented promise should rest on.

The consequence if it does not land on PassThrough: src/lib.rs only rewrites the path to pass_through_path for a RequestContext::PassThrough event, so a V1 payload classified as an HTTP event would be forwarded as a request derived from fields it does not have, instead of being POSTed as a raw payload to /events — i.e. exactly the behavior the docs rule out, and silently.

The existing test is the natural place for it:

let vpc_lattice_v1_event = json!({
    "raw_path": "/health",
    "method": "GET",
    "headers": {"accept": "*/*"},
    "query_string_parameters": {"state": "prod"},
    "body": VPC_LATTICE_BODY,
    "is_base64_encoded": false
})
.to_string();
let vpc_lattice_v1_request =
    lambda_http::request::from_str(&vpc_lattice_v1_event).expect("Failed to deserialize VPC Lattice V1 event");
assert!(matches!(
    vpc_lattice_v1_request.request_context(),
    RequestContext::PassThrough
));

let sqs_event = include_str!("../../examples/sqs-expressjs/events/sqs.json");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[GENERAL] Pulling test fixtures out of examples/ puts them outside the trigger paths of the workflows that run these tests, so edits to them get no CI signal.

The two new include_str! calls make the integration test target depend on files under examples/:

let sqs_event = include_str!("../../examples/sqs-expressjs/events/sqs.json");
// ...
let api_gateway_v1_event = include_str!("../../examples/fastapi/events/event.json");

Both .github/workflows/pr.yaml and .github/workflows/merge.yaml declare:

paths-ignore:
  - "docs/**"
  - "examples/**"

So a PR that renames, moves, or edits either JSON file runs neither cargo clippy nor cargo nextest run. A rename breaks compilation of the whole integration test target, and an edit can silently change what the assertions mean (e.g. dropping httpMethod from the fastapi event would make the ApiGatewayV1 assertion fail). Because the merge workflow ignores the same paths, the breakage does not surface when it lands on main either — it appears on the next unrelated PR, attributed to whoever opened it.

The example event files also exist to be passed to sam local invoke, so they are maintained for a different purpose and under different constraints than a test fixture pinning serde variant selection.

Copying the payloads under tests/ (e.g. tests/integ_tests/fixtures/) keeps them real recorded payloads while putting them back under CI's trigger paths and decoupling them from example maintenance.

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 = 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!(
api_gateway_v1_request.request_context(),
RequestContext::ApiGatewayV1(_)
));

let alb_event = json!({
"httpMethod": "GET",
"path": "/health",
"multiValueHeaders": {"host": ["example.com"]},
"multiValueQueryStringParameters": {"state": ["prod"]},
"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();
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_count("mode", "fast", 1)
.query_param_count("mode", "turbo", 1)
.json_body(serde_json::from_str::<serde_json::Value>(VPC_LATTICE_BODY).expect("valid JSON body"))
.is_true(move |req| {
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::<serde_json::Value>(request_context) else {
return false;
};

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");
});

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
Expand Down Expand Up @@ -1217,6 +1389,89 @@ fn add_lambda_context_to_request(request: &mut Request<Body>) {
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();
Expand Down