Description
Is there an existing issue for this?
- I have searched the existing issues
Current Behavior
With Localstack's implementation of API Gateway V1 REST API with a Lambda Integration, any request forwarded to the lambda will strip out the Stage Name from the request path to the lambda.
Expected Behavior
Within AWS, if you create a base path mapping for a custom domain using the API to leave the values of base path and stage blank, AWS automatically looks for the stage name as the first part of the path and properly forwards to a stage. This should be emulated in Localstack. Even without base path mapping, localstack is currently stripping the request URL if hitting the execute URL directly.
How are you starting LocalStack?
With a docker-compose file
Steps To Reproduce
Start up localstack using Docker Compose:
version: "3.8"
services:
localstack:
container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}"
image: localstack/localstack-pro
ports:
- "127.0.0.1:4566:4566" # LocalStack Gateway
- "127.0.0.1:4510-4559:4510-4559" # external services port range
environment:
# LocalStack configuration: https://docs.localstack.cloud/references/configuration/
- DEBUG=1
- LOCALSTACK_AUTH_TOKEN=${LOCALSTACK_AUTH_TOKEN- }
volumes:
- "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
env_file:
- .env
Use Terraform to setup an API Gateway integrating with a lambda based on a Hot Reload directory, base path mapping should have blank stage name and blank base path so that API Gateway will try to determine the stage base on the request path:
resource "aws_api_gateway_rest_api" "xxx-local" {
name = "xxx-local"
endpoint_configuration {
types = ["REGIONAL"]
}
tags = {
"_custom_id_" = "local"
}
}
resource "aws_api_gateway_resource" "xxx-local" {
parent_id = aws_api_gateway_rest_api.xxx-local.root_resource_id
rest_api_id = aws_api_gateway_rest_api.xxx-local.id
path_part = "{proxy+}"
}
resource "aws_api_gateway_method" "xxx-local" {
rest_api_id = aws_api_gateway_rest_api.xxx-local.id
resource_id = aws_api_gateway_resource.xxx-local.id
http_method = "ANY"
authorization = "NONE"
api_key_required = false
}
resource "aws_api_gateway_stage" "loyalty-api-stage" {
deployment_id = aws_api_gateway_deployment.xxx-local.id
rest_api_id = aws_api_gateway_rest_api.xxx-local.id
stage_name = "loyalty-api"
variables = {
function = "xxx-local"
}
}
resource "aws_api_gateway_method_settings" "loyalty-api-stage-settings" {
rest_api_id = aws_api_gateway_rest_api.xxx-local.id
stage_name = aws_api_gateway_stage.xxx-api-stage.stage_name
method_path = "*/*"
settings {
metrics_enabled = true
logging_level = "INFO"
}
}
resource "aws_api_gateway_integration" "loyalty-api" {
rest_api_id = aws_api_gateway_rest_api.xxx-local.id
resource_id = aws_api_gateway_resource.xxx-local.id
http_method = aws_api_gateway_method.xxx-local.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:$${stageVariables.function}/invocations"
}
resource "aws_api_gateway_deployment" "xxx-local" {
rest_api_id = aws_api_gateway_rest_api.xxx-local.id
triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_resource.xxx-local.id,
aws_api_gateway_method.xxx-local.id,
aws_api_gateway_integration.xxx-api.id,
aws_api_gateway_rest_api.xxx-local.body
]))
}
lifecycle {
create_before_destroy = true
}
depends_on = [
aws_api_gateway_integration.loyalty-api
]
}
resource "aws_api_gateway_domain_name" "xxx-local" {
domain_name = "local"
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_api_gateway_base_path_mapping" "xxx-api" {
api_id = aws_api_gateway_rest_api.xxx-local.id
domain_name = aws_api_gateway_domain_name.xxx-local.domain_name
}
Invoke via the localhost:4566/loyalty-api/users/v1/user endpoint, request comes through correctly to handlers.parse, but sometime after up to docker_runtime_exe, loyalty-api, the stage name, gets stripped from the request that is passed through to the lambda, causing a routing error since our FastAPI is expecting that stage name to determine which app flow to execute.
This happens even if calling the execute URL directly, not just the custom domain that is base path mapped
2025-02-20T21:26:31.796 DEBUG --- [et.reactor-9] l.s.a.n.execute_api.router : APIGW v1 Endpoint called
2025-02-20T21:26:31.796 DEBUG --- [et.reactor-9] l.s.a.n.e.handlers.parse : Initializing $context='{'accountId': '000000000000', 'apiId': 'local', 'deploymentId': 'vgqdn5jxlw', 'domainName': 'local.execute-api.localhost.localstack.cloud:4566', 'domainPrefix': 'local', 'extendedRequestId': '1e29f4c4', 'httpMethod': 'GET', 'identity': {'accountId': None, 'accessKey': None, 'caller': None, 'cognitoAuthenticationProvider': None, 'cognitoAuthenticationType': None, 'cognitoIdentityId': None, 'cognitoIdentityPoolId': None, 'principalOrgId': None, 'sourceIp': '127.0.0.1', 'user': None, 'userAgent': None, 'userArn': None}, 'path': '/loyalty-api/users/v1/user', 'protocol': 'HTTP/1.1', 'requestId': '1ef0094f-c726-42c0-9ea4-21c56df2af68', 'requestTime': '20/Feb/2025:21:26:31 ', 'requestTimeEpoch': 1740086791796, 'stage': 'loyalty-api'}'
2025-02-20T21:26:31.796 DEBUG --- [et.reactor-9] l.s.a.n.e.handlers.parse : Initializing $stageVariables='{'function': 'xxx-local'}'
2025-02-20T21:26:31.796 DEBUG --- [et.reactor-9] l.s.a.n.e.h.resource_route : Updating $context.resourcePath='/{proxy+}'
2025-02-20T21:26:31.796 DEBUG --- [et.reactor-9] l.s.a.n.e.h.resource_route : Updating $context.resourceId='erf3hkapil'
2025-02-20T21:26:31.796 DEBUG --- [et.reactor-9] l.s.a.n.e.header_utils : Dropping headers: []
2025-02-20T21:26:31.798 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : Got an invocation for function arn:aws:lambda:us-east-1:000000000000:function:xxx-local:$LATEST with request_id 43f3addb-41d1-489d-b02f-400846fe3676
2025-02-20T21:26:31.799 DEBUG --- [et.reactor-5] l.s.l.i.docker_runtime_exe : Sending invoke-payload '{"invoke-id": "43f3addb-41d1-489d-b02f-400846fe3676", "invoked-function-arn": "arn:aws:lambda:us-east-1:000000000000:function:xxx-local", "payload": "{\"headers\": {\"xxx-platform\": \"android\", \"X-App-DeviceId\": \"something\", \"Authorization\": \"Bearer xxx\", \"Postman-Token\": \"xxx\", \"Host\": \"local.execute-api.localhost.localstack.cloud:4566\", \"X-Forwarded-For\": \"192.168.32.1\", \"X-Forwarded-Port\": \"4566\", \"X-Forwarded-Proto\": \"HTTP\", \"Accept\": \"application/json\", \"User-Agent\": \"AmazonAPIGateway_local\", \"X-Amzn-Trace-Id\": \"Root=1-67b79e07-ad28196df2bd9031ddaddb5a;Parent=1f26407f685ec136;Sampled=0\"}, \"multiValueHeaders\": {\"xxx-platform\": [\"android\"], \"X-App-DeviceId\": [\"something\"], \"Authorization\": [\"Bearer xxx' to executor 'xxx'
2025-02-20T21:26:31.886 INFO --- [t.reactor-15] localstack.request.aws : AWS dynamodb.GetItem => 200
2025-02-20T21:26:31.895 INFO --- [et.reactor-6] localstack.request.aws : AWS dynamodb.Query => 200
2025-02-20T21:26:31.986 INFO --- [t.reactor-13] localstack.request.aws : AWS secretsmanager.GetSecretValue => 200
2025-02-20T21:26:32.117 INFO --- [t.reactor-11] localstack.request.http : POST /_localstack_lambda/3a806aa4d25876b8af25f7bfa3375a16/invocations/43f3addb-41d1-489d-b02f-400846fe3676/logs => 202
2025-02-20T21:26:32.266 INFO --- [et.reactor-3] localstack.request.http : POST /_localstack_lambda/3a806aa4d25876b8af25f7bfa3375a16/invocations/43f3addb-41d1-489d-b02f-400846fe3676/response => 202
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : Got logs for invocation '43f3addb-41d1-489d-b02f-400846fe3676'
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] START RequestId: 43f3addb-41d1-489d-b02f-400846fe3676 Version: $LATEST
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] [INFO] 2025-02-20T21:26:31.863Z 43f3addb-41d1-489d-b02f-400846fe3676{'description': 'Authentication Middleware - Dispatch', 'url': 'HTTP://local.execute-api.localhost.localstack.cloud:4566/users/v1/user', 'hostname': 'LOGGER', 'objective': 'MESSAGE', 'context': {'platform': 'android', 'device_id': 'something', 'xxx-client-ip': '', 'correlation_id': 'fec98804-2da6-4565-b7e6-5ba123778532', 'FastAPIService': 'Loyalty API'}, 'environment': 'LOCAL'}
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] [INFO] 2025-02-20T21:26:31.871Z 43f3addb-41d1-489d-b02f-400846fe3676{'description': 'Authentication Middleware - Session authenticated for user', 'phone': 'xxx', 'hostname': 'LOGGER', 'objective': 'MESSAGE', 'context': {'platform': 'android', 'device_id': 'something', 'db-client-ip': '', 'correlation_id': 'fec98804-2da6-4565-b7e6-5ba123778532', 'FastAPIService': 'Loyalty API'}, 'environment': 'LOCAL'}
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] {"level": "INFO", "logger": "app.config.logger", "message": {"description": "Authentication Middleware - Dispatch", "url": "HTTP://local.execute-api.localhost.localstack.cloud:4566/users/v1/user", "hostname": "LOGGER", "objective": "MESSAGE", "context": {"platform": "android", "device_id": "something", "db-client-ip": "", "correlation_id": "fec98804-2da6-4565-b7e6-5ba123778532", "FastAPIService": "Loyalty API"}, "environment": "LOCAL"}}
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] {"level": "INFO", "logger": "app.config.logger", "message": {"description": "Authentication Middleware - Session authenticated for user", "phone": "+xxx", "hostname": "LOGGER", "objective": "MESSAGE", "context": {"platform": "android", "device_id": "something", "xxx-client-ip": "", "correlation_id": "fec98804-2da6-4565-b7e6-5ba123778532", "FastAPIService": "Loyalty API"}, "environment": "LOCAL"}}
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] [INFO] 2025-02-20T21:26:31.888Z 43f3addb-41d1-489d-b02f-400846fe3676{'description': 'No validation user found', 'phone': 'xxx', 'hostname': 'LOGGER', 'objective': 'MESSAGE', 'context': {'platform': 'android', 'device_id': 'something', 'xxx-client-ip': '', 'correlation_id': 'fec98804-2da6-4565-b7e6-5ba123778532', 'FastAPIService': 'Loyalty API', 'firebaseUID': 'xxx'}, 'environment': 'LOCAL'}
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] [INFO] 2025-02-20T21:26:31.897Z 43f3addb-41d1-489d-b02f-400846fe3676{'description': 'Found device lock entries for phone number and device', 'phone': 'xxx', 'device': 'something', 'devicelock': [{'phone': 'xxx', 'device': 'something', 'platform': 'android', 'username': 'xxx'}], 'hostname': 'LOGGER', 'objective': 'MESSAGE', 'context': {'platform': 'android', 'device_id': 'something', 'xxx-client-ip': '', 'correlation_id': 'fec98804-2da6-4565-b7e6-5ba123778532', 'FastAPIService': 'Loyalty API', 'firebaseUID': 'xxx'}, 'environment': 'LOCAL'}
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] {"level": "INFO", "logger": "app.config.logger", "message": {"description": "No validation user found", "phone": "xxx", "hostname": "LOGGER", "objective": "MESSAGE", "context": {"platform": "android", "device_id": "something", "xxx-client-ip": "", "correlation_id": "fec98804-2da6-4565-b7e6-5ba123778532", "FastAPIService": "Loyalty API", "firebaseUID": "xxx"}, "environment": "LOCAL"}}
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] {"level": "INFO", "logger": "app.config.logger", "message": {"description": "Found device lock entries for phone number and device", "phone": "xxx", "device": "something", "devicelock": [{"phone": "xxx", "device": "something", "platform": "android", "username": "xxx"}], "hostname": "LOGGER", "objective": "MESSAGE", "context": {"platform": "android", "device_id": "something", "xxx-client-ip": "", "correlation_id": "fec98804-2da6-4565-b7e6-5ba123778532", "FastAPIService": "Loyalty API", "firebaseUID": "xxx"}, "environment": "LOCAL"}}
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] [WARNING] 2025-02-20T21:26:31.993Z 43f3addb-41d1-489d-b02f-400846fe3676{'hostname': 'xxx-Proxy', 'status': 404, 'objective': 'APP RESPONSE', 'url': '/users/v1/user', 'responseBody': {'detail': 'Not Found'}, 'context': {'platform': 'android', 'device_id': 'something', 'xxx-client-ip': '', 'correlation_id': 'fec98804-2da6-4565-b7e6-5ba123778532', 'FastAPIService': 'Loyalty API', 'firebaseUID': 'xxx', 'phone': 'xxx', 'username': 'xxx', 'deviceLock': {'phone': 'xxx', 'device': 'something', 'platform': 'android', 'username': 'xxx'}}, 'environment': 'LOCAL'}
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] {"level": "WARNING", "logger": "app.config.logger", "message": {"hostname": "xxx-Proxy", "status": 404, "objective": "APP RESPONSE", "url": "/users/v1/user", "responseBody": {"detail": "Not Found"}, "context": {"platform": "android", "device_id": "something", "xxx-client-ip": "", "correlation_id": "fec98804-2da6-4565-b7e6-5ba123778532", "FastAPIService": "Loyalty API", "firebaseUID": "xxx", "phone": "xxx", "username": "xxx", "deviceLock": {"phone": "", "device": "something", "platform": "android", "username": "xxx"}}, "environment": "LOCAL"}}
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] END RequestId: 43f3addb-41d1-489d-b02f-400846fe3676
2025-02-20T21:26:32.268 DEBUG --- [et.reactor-5] l.s.l.i.version_manager : [xxx-local-43f3addb-41d1-489d-b02f-400846fe3676] REPORT RequestId: 43f3addb-41d1-489d-b02f-400846fe3676 Duration: 250.77 ms Billed Duration: 251 ms Memory Size: 128 MB Max Memory Used: 128 MB
2025-02-20T21:26:32.269 DEBUG --- [et.reactor-5] l.s.lambda_.provider : Lambda invocation duration: 470.18ms
2025-02-20T21:26:32.271 DEBUG --- [et.reactor-9] l.s.a.n.e.h.method_respons : Remapping header: Content-Length
2025-02-20T21:26:32.271 DEBUG --- [et.reactor-9] l.s.a.n.e.header_utils : Dropping headers: []
2025-02-20T21:26:32.271 INFO --- [et.reactor-9] localstack.request.http : GET /loyalty-api/users/v1/user => 404
Unfortunately can't provide you lambda code to help get to this point, but I think the above illustrates what's going on currently.
Within AWS this is a perfectly valid setup that works as expected based on AWS documentation. Is there a particular reason localstack strips the stage name or is this something that can be fixed?
Environment
- OS: MacOS
- LocalStack:
LocalStack version: 4.0.4.dev117
LocalStack build date: 2025-01-22
LocalStack build git hash: 648677eae
Anything else?
https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-mappings.html