Skip to content

Commit c35e80f

Browse files
rubenfonsecaheitorlessa
authored andcommitted
fix(event_handler): prioritize static over dynamic route to prevent order of route registration mismatch (aws-powertools#2458)
Co-authored-by: heitorlessa <[email protected]>
1 parent 869bc34 commit c35e80f

File tree

2 files changed

+39
-5
lines changed

2 files changed

+39
-5
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,8 @@ def __init__(
483483
with api gateways with multiple custom mappings.
484484
"""
485485
self._proxy_type = proxy_type
486-
self._routes: List[Route] = []
486+
self._dynamic_routes: List[Route] = []
487+
self._static_routes: List[Route] = []
487488
self._route_keys: List[str] = []
488489
self._exception_handlers: Dict[Type, Callable] = {}
489490
self._cors = cors
@@ -515,7 +516,17 @@ def register_resolver(func: Callable):
515516
cors_enabled = cors
516517

517518
for item in methods:
518-
self._routes.append(Route(item, self._compile_regex(rule), func, cors_enabled, compress, cache_control))
519+
_route = Route(item, self._compile_regex(rule), func, cors_enabled, compress, cache_control)
520+
521+
# The more specific route wins.
522+
# We store dynamic (/studies/{studyid}) and static routes (/studies/fetch) separately.
523+
# Then attempt a match for static routes before dynamic routes.
524+
# This ensures that the most specific route is prioritized and processed first (studies/fetch).
525+
if _route.rule.groups > 0:
526+
self._dynamic_routes.append(_route)
527+
else:
528+
self._static_routes.append(_route)
529+
519530
route_key = item + rule
520531
if route_key in self._route_keys:
521532
warnings.warn(
@@ -624,7 +635,7 @@ def _resolve(self) -> ResponseBuilder:
624635
"""Resolves the response or return the not found response"""
625636
method = self.current_event.http_method.upper()
626637
path = self._remove_prefix(self.current_event.path)
627-
for route in self._routes:
638+
for route in self._static_routes + self._dynamic_routes:
628639
if method != route.method:
629640
continue
630641
match_results: Optional[Match] = route.rule.match(path)

tests/functional/event_handler/test_api_gateway.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def handler(event, context):
298298
return app.resolve(event, context)
299299

300300
# Also check the route configurations
301-
routes = app._routes
301+
routes = app._static_routes
302302
assert len(routes) == 5
303303
for route in routes:
304304
if route.func == get_func:
@@ -1205,7 +1205,7 @@ def patch_func():
12051205
app.include_router(router)
12061206

12071207
# Also check check the route configurations
1208-
routes = app._routes
1208+
routes = app._static_routes
12091209
assert len(routes) == 5
12101210
for route in routes:
12111211
if route.func == get_func:
@@ -1647,3 +1647,26 @@ def get_message():
16471647
assert response["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON]
16481648
response_body = json.loads(response["body"])
16491649
assert response_body["message"] == "success"
1650+
1651+
1652+
def test_route_match_prioritize_full_match():
1653+
# GIVEN a Http API V1, with a function registered with two routes
1654+
app = APIGatewayRestResolver()
1655+
router = Router()
1656+
1657+
@router.get("/my/{path}")
1658+
def dynamic_handler() -> Response:
1659+
return Response(200, content_types.APPLICATION_JSON, json.dumps({"hello": "dynamic"}))
1660+
1661+
@router.get("/my/path")
1662+
def static_handler() -> Response:
1663+
return Response(200, content_types.APPLICATION_JSON, json.dumps({"hello": "static"}))
1664+
1665+
app.include_router(router)
1666+
1667+
# WHEN calling the event handler with /foo/dynamic
1668+
response = app(LOAD_GW_EVENT, {})
1669+
1670+
# THEN the static_handler should have been called, because it fully matches the path directly
1671+
response_body = json.loads(response["body"])
1672+
assert response_body["hello"] == "static"

0 commit comments

Comments
 (0)