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
37 changes: 37 additions & 0 deletions tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,3 +689,40 @@ def test_wsgi_environ_fixes_double_prefix_bug(self):
self.assertIn("SCRIPT_NAME='/dev'", response["body"])
self.assertIn("PATH_INFO='/debug/wsgi/environ'", response["body"]) # FIXED: stage stripped
self.assertIn("/dev/debug/wsgi/environ", response["body"]) # Correct single prefix!

def test_wsgi_v2_custom_domain_no_double_stage(self):
"""
Test that API Gateway v2 with a custom domain mapping doesn't double the
stage in URLs (#1409). When a custom domain maps to a stage, API Gateway
strips the stage from rawPath, so SCRIPT_NAME should be empty.
"""
lh = LambdaHandler("tests.test_wsgi_script_name_settings")

event = {
"version": "2.0",
"routeKey": "$default",
"rawPath": "/return/request/url", # Custom domain: stage already stripped
"rawQueryString": "",
"headers": {
"host": "api.example.com",
},
"requestContext": {
"http": {
"method": "GET",
"path": "/return/request/url",
},
"stage": "dev", # Stage is still present in requestContext
"domainName": "api.example.com",
},
"isBase64Encoded": False,
"body": "",
}
response = lh.handler(event, None)

self.assertEqual(response["statusCode"], 200)
# With custom domain, stage is NOT in rawPath, so SCRIPT_NAME should be empty
# and the URL should NOT contain /dev prefix
self.assertEqual(
response["body"],
"https://api.example.com/return/request/url",
)
13 changes: 8 additions & 5 deletions zappa/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,13 +711,16 @@ def handler(self, event, context):
stage = request_context.get("stage")

# For API Gateway v2, the stage is included in rawPath and we need to
# set script_name so it can be stripped from PATH_INFO
# For Function URLs (no stage), leave script_name empty
if stage:
# API Gateway v2 with named stage - rawPath includes the stage
# set script_name so it can be stripped from PATH_INFO.
# When using a custom domain with API mapping, API Gateway already
# strips the stage from rawPath, so we detect this and skip setting
# script_name to avoid double-stage redirects (#1409).
raw_path = event.get("rawPath", "")
if stage and raw_path.startswith(f"/{stage}"):
# Direct API Gateway v2 access - rawPath includes the stage
script_name = f"/{stage}"
else:
# Function URL - no stage
# Function URL or custom domain (stage already stripped)
script_name = ""

# ASGI path
Expand Down
Loading