diff --git a/tests/test_handler.py b/tests/test_handler.py index 353baf5d..8260aa0d 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -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", + ) diff --git a/zappa/handler.py b/zappa/handler.py index 11319dd2..d1f2db56 100644 --- a/zappa/handler.py +++ b/zappa/handler.py @@ -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