Skip to content

Commit 91e1cec

Browse files
royassisRoy Assisrubenfonseca
authored andcommitted
feat(event_handler): allow stripping route prefixes using regexes (aws-powertools#2521)
Co-authored-by: Roy Assis <[email protected]> Co-authored-by: Ruben Fonseca <[email protected]>
1 parent 8ba31a3 commit 91e1cec

File tree

6 files changed

+90
-16
lines changed

6 files changed

+90
-16
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

+23-11
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ def __init__(
520520
cors: Optional[CORSConfig] = None,
521521
debug: Optional[bool] = None,
522522
serializer: Optional[Callable[[Dict], str]] = None,
523-
strip_prefixes: Optional[List[str]] = None,
523+
strip_prefixes: Optional[List[Union[str, Pattern]]] = None,
524524
):
525525
"""
526526
Parameters
@@ -534,9 +534,10 @@ def __init__(
534534
environment variable
535535
serializer : Callable, optional
536536
function to serialize `obj` to a JSON formatted `str`, by default json.dumps
537-
strip_prefixes: List[str], optional
538-
optional list of prefixes to be removed from the request path before doing the routing. This is often used
539-
with api gateways with multiple custom mappings.
537+
strip_prefixes: List[Union[str, Pattern]], optional
538+
optional list of prefixes to be removed from the request path before doing the routing.
539+
This is often used with api gateways with multiple custom mappings.
540+
Each prefix can be a static string or a compiled regex pattern
540541
"""
541542
self._proxy_type = proxy_type
542543
self._dynamic_routes: List[Route] = []
@@ -713,10 +714,21 @@ def _remove_prefix(self, path: str) -> str:
713714
return path
714715

715716
for prefix in self._strip_prefixes:
716-
if path == prefix:
717-
return "/"
718-
if self._path_starts_with(path, prefix):
719-
return path[len(prefix) :]
717+
if isinstance(prefix, str):
718+
if path == prefix:
719+
return "/"
720+
721+
if self._path_starts_with(path, prefix):
722+
return path[len(prefix) :]
723+
724+
if isinstance(prefix, Pattern):
725+
path = re.sub(prefix, "", path)
726+
727+
# When using regexes, we might get into a point where everything is removed
728+
# from the string, so we check if it's empty and return /, since there's nothing
729+
# else to strip anymore.
730+
if not path:
731+
return "/"
720732

721733
return path
722734

@@ -911,7 +923,7 @@ def __init__(
911923
cors: Optional[CORSConfig] = None,
912924
debug: Optional[bool] = None,
913925
serializer: Optional[Callable[[Dict], str]] = None,
914-
strip_prefixes: Optional[List[str]] = None,
926+
strip_prefixes: Optional[List[Union[str, Pattern]]] = None,
915927
):
916928
"""Amazon API Gateway REST and HTTP API v1 payload resolver"""
917929
super().__init__(ProxyEventType.APIGatewayProxyEvent, cors, debug, serializer, strip_prefixes)
@@ -942,7 +954,7 @@ def __init__(
942954
cors: Optional[CORSConfig] = None,
943955
debug: Optional[bool] = None,
944956
serializer: Optional[Callable[[Dict], str]] = None,
945-
strip_prefixes: Optional[List[str]] = None,
957+
strip_prefixes: Optional[List[Union[str, Pattern]]] = None,
946958
):
947959
"""Amazon API Gateway HTTP API v2 payload resolver"""
948960
super().__init__(ProxyEventType.APIGatewayProxyEventV2, cors, debug, serializer, strip_prefixes)
@@ -956,7 +968,7 @@ def __init__(
956968
cors: Optional[CORSConfig] = None,
957969
debug: Optional[bool] = None,
958970
serializer: Optional[Callable[[Dict], str]] = None,
959-
strip_prefixes: Optional[List[str]] = None,
971+
strip_prefixes: Optional[List[Union[str, Pattern]]] = None,
960972
):
961973
"""Amazon Application Load Balancer (ALB) resolver"""
962974
super().__init__(ProxyEventType.ALBEvent, cors, debug, serializer, strip_prefixes)

aws_lambda_powertools/event_handler/lambda_function_url.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Callable, Dict, List, Optional
1+
from typing import Callable, Dict, List, Optional, Pattern, Union
22

33
from aws_lambda_powertools.event_handler import CORSConfig
44
from aws_lambda_powertools.event_handler.api_gateway import (
@@ -51,6 +51,6 @@ def __init__(
5151
cors: Optional[CORSConfig] = None,
5252
debug: Optional[bool] = None,
5353
serializer: Optional[Callable[[Dict], str]] = None,
54-
strip_prefixes: Optional[List[str]] = None,
54+
strip_prefixes: Optional[List[Union[str, Pattern]]] = None,
5555
):
5656
super().__init__(ProxyEventType.LambdaFunctionUrlEvent, cors, debug, serializer, strip_prefixes)

aws_lambda_powertools/event_handler/vpc_lattice.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Callable, Dict, List, Optional
1+
from typing import Callable, Dict, List, Optional, Pattern, Union
22

33
from aws_lambda_powertools.event_handler import CORSConfig
44
from aws_lambda_powertools.event_handler.api_gateway import (
@@ -47,7 +47,7 @@ def __init__(
4747
cors: Optional[CORSConfig] = None,
4848
debug: Optional[bool] = None,
4949
serializer: Optional[Callable[[Dict], str]] = None,
50-
strip_prefixes: Optional[List[str]] = None,
50+
strip_prefixes: Optional[List[Union[str, Pattern]]] = None,
5151
):
5252
"""Amazon VPC Lattice resolver"""
5353
super().__init__(ProxyEventType.VPCLatticeEvent, cors, debug, serializer, strip_prefixes)

docs/core/event_handler/api_gateway.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ When using [Custom Domain API Mappings feature](https://docs.aws.amazon.com/apig
272272

273273
**Scenario**: You have a custom domain `api.mydomain.dev`. Then you set `/payment` API Mapping to forward any payment requests to your Payments API.
274274

275-
**Challenge**: This means your `path` value for any API requests will always contain `/payment/<actual_request>`, leading to HTTP 404 as Event Handler is trying to match what's after `payment/`. This gets further complicated with an [arbitrary level of nesting](https://github.com/aws-powertools/powertools-lambda-roadmap/issues/34){target="_blank"}.
275+
**Challenge**: This means your `path` value for any API requests will always contain `/payment/<actual_request>`, leading to HTTP 404 as Event Handler is trying to match what's after `payment/`. This gets further complicated with an [arbitrary level of nesting](https://github.com/aws-powertools/powertools-lambda/issues/34){target="_blank"}.
276276

277277
To address this API Gateway behavior, we use `strip_prefixes` parameter to account for these prefixes that are now injected into the path regardless of which type of API Gateway you're using.
278278

@@ -293,6 +293,14 @@ To address this API Gateway behavior, we use `strip_prefixes` parameter to accou
293293

294294
For example, when using `strip_prefixes` value of `/pay`, there is no difference between a request path of `/pay` and `/pay/`; and the path argument would be defined as `/`.
295295

296+
For added flexibility, you can use regexes to strip a prefix. This is helpful when you have many options due to different combinations of prefixes (e.g: multiple environments, multiple versions).
297+
298+
=== "strip_route_prefix_regex.py"
299+
300+
```python hl_lines="12"
301+
--8<-- "examples/event_handler_rest/src/strip_route_prefix_regex.py"
302+
```
303+
296304
## Advanced
297305

298306
### CORS
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import re
2+
3+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
4+
from aws_lambda_powertools.utilities.typing import LambdaContext
5+
6+
# This will support:
7+
# /v1/dev/subscriptions/<subscription>
8+
# /v1/stg/subscriptions/<subscription>
9+
# /v1/qa/subscriptions/<subscription>
10+
# /v2/dev/subscriptions/<subscription>
11+
# ...
12+
app = APIGatewayRestResolver(strip_prefixes=[re.compile(r"/v[1-3]+/(dev|stg|qa)")])
13+
14+
15+
@app.get("/subscriptions/<subscription>")
16+
def get_subscription(subscription):
17+
return {"subscription_id": subscription}
18+
19+
20+
def lambda_handler(event: dict, context: LambdaContext) -> dict:
21+
return app.resolve(event, context)

tests/functional/event_handler/test_api_gateway.py

+33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import json
3+
import re
34
import zlib
45
from copy import deepcopy
56
from decimal import Decimal
@@ -1077,6 +1078,38 @@ def foo():
10771078
assert response["statusCode"] == 200
10781079

10791080

1081+
@pytest.mark.parametrize(
1082+
"path",
1083+
[
1084+
pytest.param("/stg/foo", id="path matched pay prefix"),
1085+
pytest.param("/dev/foo", id="path matched pay prefix with multiple numbers"),
1086+
pytest.param("/foo", id="path does not start with any of the prefixes"),
1087+
],
1088+
)
1089+
def test_remove_prefix_by_regex(path: str):
1090+
app = ApiGatewayResolver(strip_prefixes=[re.compile(r"/(dev|stg)")])
1091+
1092+
@app.get("/foo")
1093+
def foo():
1094+
...
1095+
1096+
response = app({"httpMethod": "GET", "path": path}, None)
1097+
1098+
assert response["statusCode"] == 200
1099+
1100+
1101+
def test_empty_path_when_using_regexes():
1102+
app = ApiGatewayResolver(strip_prefixes=[re.compile(r"/(dev|stg)")])
1103+
1104+
@app.get("/")
1105+
def foo():
1106+
...
1107+
1108+
response = app({"httpMethod": "GET", "path": "/dev"}, None)
1109+
1110+
assert response["statusCode"] == 200
1111+
1112+
10801113
@pytest.mark.parametrize(
10811114
"prefix",
10821115
[

0 commit comments

Comments
 (0)