Skip to content

Commit 3fa6803

Browse files
author
Michael Brewer
authored
feat(event-handler): prefixes to strip for custom mappings (#579)
1 parent 61e7f2d commit 3fa6803

File tree

2 files changed

+82
-2
lines changed

2 files changed

+82
-2
lines changed

Diff for: aws_lambda_powertools/event_handler/api_gateway.py

+28-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class ProxyEventType(Enum):
3737
ALBEvent = "ALBEvent"
3838

3939

40-
class CORSConfig(object):
40+
class CORSConfig:
4141
"""CORS Config
4242
4343
Examples
@@ -265,6 +265,7 @@ def __init__(
265265
cors: Optional[CORSConfig] = None,
266266
debug: Optional[bool] = None,
267267
serializer: Optional[Callable[[Dict], str]] = None,
268+
strip_prefixes: Optional[List[str]] = None,
268269
):
269270
"""
270271
Parameters
@@ -276,6 +277,11 @@ def __init__(
276277
debug: Optional[bool]
277278
Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_EVENT_HANDLER_DEBUG"
278279
environment variable
280+
serializer : Callable, optional
281+
function to serialize `obj` to a JSON formatted `str`, by default json.dumps
282+
strip_prefixes: List[str], optional
283+
optional list of prefixes to be removed from the request path before doing the routing. This is often used
284+
with api gateways with multiple custom mappings.
279285
"""
280286
self._proxy_type = proxy_type
281287
self._routes: List[Route] = []
@@ -285,6 +291,7 @@ def __init__(
285291
self._debug = resolve_truthy_env_var_choice(
286292
env=os.getenv(constants.EVENT_HANDLER_DEBUG_ENV, "false"), choice=debug
287293
)
294+
self._strip_prefixes = strip_prefixes
288295

289296
# Allow for a custom serializer or a concise json serialization
290297
self._serializer = serializer or partial(json.dumps, separators=(",", ":"), cls=Encoder)
@@ -521,7 +528,7 @@ def _to_proxy_event(self, event: Dict) -> BaseProxyEvent:
521528
def _resolve(self) -> ResponseBuilder:
522529
"""Resolves the response or return the not found response"""
523530
method = self.current_event.http_method.upper()
524-
path = self.current_event.path
531+
path = self._remove_prefix(self.current_event.path)
525532
for route in self._routes:
526533
if method != route.method:
527534
continue
@@ -533,6 +540,25 @@ def _resolve(self) -> ResponseBuilder:
533540
logger.debug(f"No match found for path {path} and method {method}")
534541
return self._not_found(method)
535542

543+
def _remove_prefix(self, path: str) -> str:
544+
"""Remove the configured prefix from the path"""
545+
if not isinstance(self._strip_prefixes, list):
546+
return path
547+
548+
for prefix in self._strip_prefixes:
549+
if self._path_starts_with(path, prefix):
550+
return path[len(prefix) :]
551+
552+
return path
553+
554+
@staticmethod
555+
def _path_starts_with(path: str, prefix: str):
556+
"""Returns true if the `path` starts with a prefix plus a `/`"""
557+
if not isinstance(prefix, str) or len(prefix) == 0:
558+
return False
559+
560+
return path.startswith(prefix + "/")
561+
536562
def _not_found(self, method: str) -> ResponseBuilder:
537563
"""Called when no matching route was found and includes support for the cors preflight response"""
538564
headers = {}

Diff for: tests/functional/event_handler/test_api_gateway.py

+54
Original file line numberDiff line numberDiff line change
@@ -769,3 +769,57 @@ def get_color() -> Dict:
769769
body = response["body"]
770770
expected = '{"color": 1, "variations": ["dark", "light"]}'
771771
assert expected == body
772+
773+
774+
@pytest.mark.parametrize(
775+
"path",
776+
[
777+
pytest.param("/pay/foo", id="path matched pay prefix"),
778+
pytest.param("/payment/foo", id="path matched payment prefix"),
779+
pytest.param("/foo", id="path does not start with any of the prefixes"),
780+
],
781+
)
782+
def test_remove_prefix(path: str):
783+
# GIVEN events paths `/pay/foo`, `/payment/foo` or `/foo`
784+
# AND a configured strip_prefixes of `/pay` and `/payment`
785+
app = ApiGatewayResolver(strip_prefixes=["/pay", "/payment"])
786+
787+
@app.get("/pay/foo")
788+
def pay_foo():
789+
raise ValueError("should not be matching")
790+
791+
@app.get("/foo")
792+
def foo():
793+
...
794+
795+
# WHEN calling handler
796+
response = app({"httpMethod": "GET", "path": path}, None)
797+
798+
# THEN a route for `/foo` should be found
799+
assert response["statusCode"] == 200
800+
801+
802+
@pytest.mark.parametrize(
803+
"prefix",
804+
[
805+
pytest.param("/foo", id="String are not supported"),
806+
pytest.param({"/foo"}, id="Sets are not supported"),
807+
pytest.param({"foo": "/foo"}, id="Dicts are not supported"),
808+
pytest.param(tuple("/foo"), id="Tuples are not supported"),
809+
pytest.param([None, 1, "", False], id="List of invalid values"),
810+
],
811+
)
812+
def test_ignore_invalid(prefix):
813+
# GIVEN an invalid prefix
814+
app = ApiGatewayResolver(strip_prefixes=prefix)
815+
816+
@app.get("/foo/status")
817+
def foo():
818+
...
819+
820+
# WHEN calling handler
821+
response = app({"httpMethod": "GET", "path": "/foo/status"}, None)
822+
823+
# THEN a route for `/foo/status` should be found
824+
# so no prefix was stripped from the request path
825+
assert response["statusCode"] == 200

0 commit comments

Comments
 (0)