diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 05fbc1c06c1..962fd51ccf4 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -483,7 +483,8 @@ def __init__( with api gateways with multiple custom mappings. """ self._proxy_type = proxy_type - self._routes: List[Route] = [] + self._dynamic_routes: List[Route] = [] + self._static_routes: List[Route] = [] self._route_keys: List[str] = [] self._exception_handlers: Dict[Type, Callable] = {} self._cors = cors @@ -515,7 +516,17 @@ def register_resolver(func: Callable): cors_enabled = cors for item in methods: - self._routes.append(Route(item, self._compile_regex(rule), func, cors_enabled, compress, cache_control)) + _route = Route(item, self._compile_regex(rule), func, cors_enabled, compress, cache_control) + + # The more specific route wins. + # We store dynamic (/studies/{studyid}) and static routes (/studies/fetch) separately. + # Then attempt a match for static routes before dynamic routes. + # This ensures that the most specific route is prioritized and processed first (studies/fetch). + if _route.rule.groups > 0: + self._dynamic_routes.append(_route) + else: + self._static_routes.append(_route) + route_key = item + rule if route_key in self._route_keys: warnings.warn( @@ -624,7 +635,7 @@ def _resolve(self) -> ResponseBuilder: """Resolves the response or return the not found response""" method = self.current_event.http_method.upper() path = self._remove_prefix(self.current_event.path) - for route in self._routes: + for route in self._static_routes + self._dynamic_routes: if method != route.method: continue match_results: Optional[Match] = route.rule.match(path) diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 6faad88d7f1..c17422f8d94 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -298,7 +298,7 @@ def handler(event, context): return app.resolve(event, context) # Also check the route configurations - routes = app._routes + routes = app._static_routes assert len(routes) == 5 for route in routes: if route.func == get_func: @@ -1205,7 +1205,7 @@ def patch_func(): app.include_router(router) # Also check check the route configurations - routes = app._routes + routes = app._static_routes assert len(routes) == 5 for route in routes: if route.func == get_func: @@ -1647,3 +1647,26 @@ def get_message(): assert response["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON] response_body = json.loads(response["body"]) assert response_body["message"] == "success" + + +def test_route_match_prioritize_full_match(): + # GIVEN a Http API V1, with a function registered with two routes + app = APIGatewayRestResolver() + router = Router() + + @router.get("/my/{path}") + def dynamic_handler() -> Response: + return Response(200, content_types.APPLICATION_JSON, json.dumps({"hello": "dynamic"})) + + @router.get("/my/path") + def static_handler() -> Response: + return Response(200, content_types.APPLICATION_JSON, json.dumps({"hello": "static"})) + + app.include_router(router) + + # WHEN calling the event handler with /foo/dynamic + response = app(LOAD_GW_EVENT, {}) + + # THEN the static_handler should have been called, because it fully matches the path directly + response_body = json.loads(response["body"]) + assert response_body["hello"] == "static"