Skip to content

Commit b521841

Browse files
committed
fix(api-gateway): route pattern regex aws-powertools#520
1 parent 018a91b commit b521841

File tree

2 files changed

+71
-3
lines changed

2 files changed

+71
-3
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

+33-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020

2121
logger = logging.getLogger(__name__)
2222

23+
_DYNAMIC_ROUTE_PATTERN = r"(<\w+>)"
24+
_NAMED_GROUP_BOUNDARY_PATTERN = r"(?P\1\\w+\\b)"
25+
2326

2427
class ProxyEventType(Enum):
2528
"""An enumerations of the supported proxy event types."""
@@ -460,8 +463,35 @@ def __call__(self, event, context) -> Any:
460463

461464
@staticmethod
462465
def _compile_regex(rule: str):
463-
"""Precompile regex pattern"""
464-
rule_regex: str = re.sub(r"(<\w+>)", r"(?P\1.+)", rule)
466+
"""Precompile regex pattern
467+
468+
Logic
469+
-----
470+
471+
1. Find any dynamic routes defined as <pattern>
472+
e.g. @app.get("/accounts/<account_id>")
473+
2. Create a new regex by substituting every dynamic route found as a named group (?P<group>),
474+
and match whole words only (word boundary) instead of a greedy match
475+
476+
non-greedy example with word boundary
477+
478+
rule: '/accounts/<account_id>'
479+
regex: r'/accounts/(?P<account_id>\\w+\\b)'
480+
481+
value: /accounts/123/some_other_path
482+
account_id: 123
483+
484+
greedy example without word boundary
485+
486+
regex: r'/accounts/(?P<account_id>.+)'
487+
488+
value: /accounts/123/some_other_path
489+
account_id: 123/some_other_path
490+
3. Compiles a regex and include start (^) and end ($) in between for an exact match
491+
492+
NOTE: See #520 for context
493+
"""
494+
rule_regex: str = re.sub(_DYNAMIC_ROUTE_PATTERN, _NAMED_GROUP_BOUNDARY_PATTERN, rule)
465495
return re.compile("^{}$".format(rule_regex))
466496

467497
def _to_proxy_event(self, event: Dict) -> BaseProxyEvent:
@@ -485,7 +515,7 @@ def _resolve(self) -> ResponseBuilder:
485515
match: Optional[re.Match] = route.rule.match(path)
486516
if match:
487517
logger.debug("Found a registered route. Calling function")
488-
return self._call_route(route, match.groupdict())
518+
return self._call_route(route, match.groupdict()) # pass fn args
489519

490520
logger.debug(f"No match found for path {path} and method {method}")
491521
return self._not_found(method)

tests/functional/event_handler/test_api_gateway.py

+38
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import base64
22
import json
33
import zlib
4+
from copy import deepcopy
45
from decimal import Decimal
56
from pathlib import Path
67
from typing import Dict
@@ -664,3 +665,40 @@ def test_debug_print_event(capsys):
664665
# THEN print the event
665666
out, err = capsys.readouterr()
666667
assert json.loads(out) == event
668+
669+
670+
def test_similar_dynamic_routes():
671+
# GIVEN
672+
app = ApiGatewayResolver()
673+
event = deepcopy(LOAD_GW_EVENT)
674+
675+
# r'^/accounts/(?P<account_id>\\w+\\b)$' # noqa: E800
676+
@app.get("/accounts/<account_id>")
677+
def get_account(account_id: str):
678+
assert account_id == "single_account"
679+
return {"message": f"{account_id}"}
680+
681+
# r'^/accounts/(?P<account_id>\\w+\\b)/source_networks$' # noqa: E800
682+
@app.get("/accounts/<account_id>/source_networks")
683+
def get_account_networks(account_id: str):
684+
assert account_id == "nested_account"
685+
return {"message": f"{account_id}"}
686+
687+
# r'^/accounts/(?P<account_id>\\w+\\b)/source_networks/(?P<network_id>\\w+\\b)$' # noqa: E800
688+
@app.get("/accounts/<account_id>/source_networks/<network_id>")
689+
def get_network_account(account_id: str, network_id: str):
690+
assert account_id == "nested_account"
691+
assert network_id == "network"
692+
return {"message": f"{account_id}"}
693+
694+
event["resource"] = "/accounts/{account_id}/source_networks"
695+
event["path"] = "/accounts/nested_account/source_networks"
696+
app.resolve(event, None)
697+
698+
event["resource"] = "/accounts/{account_id}"
699+
event["path"] = "/accounts/single_account"
700+
app.resolve(event, None)
701+
702+
event["resource"] = "/accounts/{account_id}/source_networks/{network_id}"
703+
event["path"] = "/accounts/nested_account/source_networks/network"
704+
app.resolve(event, {})

0 commit comments

Comments
 (0)