From 6c38ef63ac1061cee7ce78b1284a90627a570a0d Mon Sep 17 00:00:00 2001 From: Roy Assis Date: Thu, 22 Jun 2023 13:22:01 +0000 Subject: [PATCH 1/6] Update _remove_prefix method to iterativly remove prefix by regex --- aws_lambda_powertools/event_handler/api_gateway.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 446b1eca856..9e5b2ce7d0f 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -715,8 +715,10 @@ def _remove_prefix(self, path: str) -> str: for prefix in self._strip_prefixes: if path == prefix: return "/" - if self._path_starts_with(path, prefix): - return path[len(prefix) :] + path = re.sub(rf"^/?({prefix})/", r"/", path) + + if not path: + path = "/" return path From 411dc2bf0821120bc1b2a360358c6756d9da39cc Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Thu, 10 Aug 2023 14:50:25 +0200 Subject: [PATCH 2/6] fix: streamlined regexes --- .../event_handler/api_gateway.py | 20 ++++++++++++------ .../event_handler/test_api_gateway.py | 21 +++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 9e5b2ce7d0f..8fa7f5d0b3b 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -520,7 +520,7 @@ def __init__( cors: Optional[CORSConfig] = None, debug: Optional[bool] = None, serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, + strip_prefixes: Optional[List[Union[str, Pattern]]] = None, ): """ Parameters @@ -534,9 +534,9 @@ def __init__( environment variable serializer : Callable, optional function to serialize `obj` to a JSON formatted `str`, by default json.dumps - strip_prefixes: List[str], optional + strip_prefixes: List[Union[str, Pattern]], optional optional list of prefixes to be removed from the request path before doing the routing. This is often used - with api gateways with multiple custom mappings. + with api gateways with multiple custom mappings. Each prefix can be a static string or a compiled regex """ self._proxy_type = proxy_type self._dynamic_routes: List[Route] = [] @@ -713,10 +713,18 @@ def _remove_prefix(self, path: str) -> str: return path for prefix in self._strip_prefixes: - if path == prefix: - return "/" - path = re.sub(rf"^/?({prefix})/", r"/", path) + if isinstance(prefix, str): + if path == prefix: + return "/" + + if self._path_starts_with(path, prefix): + return path[len(prefix) :] + + if isinstance(prefix, Pattern): + path = re.sub(prefix, "", path) + # When using regexes, we might get into a point where everything is removed + # from the string, so we check if it's empty and change it accordingly. if not path: path = "/" diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 26c71e1f27d..f40bb2439ee 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -1,5 +1,6 @@ import base64 import json +import re import zlib from copy import deepcopy from decimal import Decimal @@ -1077,6 +1078,26 @@ def foo(): assert response["statusCode"] == 200 +@pytest.mark.parametrize( + "path", + [ + pytest.param("/stg/foo", id="path matched pay prefix"), + pytest.param("/dev/foo", id="path matched pay prefix with multiple numbers"), + pytest.param("/foo", id="path does not start with any of the prefixes"), + ], +) +def test_remove_prefix_by_regex(path: str): + app = ApiGatewayResolver(strip_prefixes=[re.compile(r"/(dev|stg)")]) + + @app.get("/foo") + def foo(): + ... + + response = app({"httpMethod": "GET", "path": path}, None) + + assert response["statusCode"] == 200 + + @pytest.mark.parametrize( "prefix", [ From 49f299b8cd2e0ee1238f1edbee8b2bd3f2a8e6ab Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Thu, 10 Aug 2023 15:56:13 +0200 Subject: [PATCH 3/6] fix: update APIGateway subclasses --- aws_lambda_powertools/event_handler/api_gateway.py | 6 +++--- aws_lambda_powertools/event_handler/lambda_function_url.py | 4 ++-- aws_lambda_powertools/event_handler/vpc_lattice.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 8fa7f5d0b3b..72f280b3ea2 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -921,7 +921,7 @@ def __init__( cors: Optional[CORSConfig] = None, debug: Optional[bool] = None, serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, + strip_prefixes: Optional[List[Union[str, Pattern]]] = None, ): """Amazon API Gateway REST and HTTP API v1 payload resolver""" super().__init__(ProxyEventType.APIGatewayProxyEvent, cors, debug, serializer, strip_prefixes) @@ -952,7 +952,7 @@ def __init__( cors: Optional[CORSConfig] = None, debug: Optional[bool] = None, serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, + strip_prefixes: Optional[List[Union[str, Pattern]]] = None, ): """Amazon API Gateway HTTP API v2 payload resolver""" super().__init__(ProxyEventType.APIGatewayProxyEventV2, cors, debug, serializer, strip_prefixes) @@ -966,7 +966,7 @@ def __init__( cors: Optional[CORSConfig] = None, debug: Optional[bool] = None, serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, + strip_prefixes: Optional[List[Union[str, Pattern]]] = None, ): """Amazon Application Load Balancer (ALB) resolver""" super().__init__(ProxyEventType.ALBEvent, cors, debug, serializer, strip_prefixes) diff --git a/aws_lambda_powertools/event_handler/lambda_function_url.py b/aws_lambda_powertools/event_handler/lambda_function_url.py index 6978b29f451..433a013ab0b 100644 --- a/aws_lambda_powertools/event_handler/lambda_function_url.py +++ b/aws_lambda_powertools/event_handler/lambda_function_url.py @@ -1,4 +1,4 @@ -from typing import Callable, Dict, List, Optional +from typing import Callable, Dict, List, Optional, Pattern, Union from aws_lambda_powertools.event_handler import CORSConfig from aws_lambda_powertools.event_handler.api_gateway import ( @@ -51,6 +51,6 @@ def __init__( cors: Optional[CORSConfig] = None, debug: Optional[bool] = None, serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, + strip_prefixes: Optional[List[Union[str, Pattern]]] = None, ): super().__init__(ProxyEventType.LambdaFunctionUrlEvent, cors, debug, serializer, strip_prefixes) diff --git a/aws_lambda_powertools/event_handler/vpc_lattice.py b/aws_lambda_powertools/event_handler/vpc_lattice.py index 1150f7224fb..b3cb042b40b 100644 --- a/aws_lambda_powertools/event_handler/vpc_lattice.py +++ b/aws_lambda_powertools/event_handler/vpc_lattice.py @@ -1,4 +1,4 @@ -from typing import Callable, Dict, List, Optional +from typing import Callable, Dict, List, Optional, Pattern, Union from aws_lambda_powertools.event_handler import CORSConfig from aws_lambda_powertools.event_handler.api_gateway import ( @@ -47,7 +47,7 @@ def __init__( cors: Optional[CORSConfig] = None, debug: Optional[bool] = None, serializer: Optional[Callable[[Dict], str]] = None, - strip_prefixes: Optional[List[str]] = None, + strip_prefixes: Optional[List[Union[str, Pattern]]] = None, ): """Amazon VPC Lattice resolver""" super().__init__(ProxyEventType.VPCLatticeEvent, cors, debug, serializer, strip_prefixes) From 276293b278fb8ebd46fa3c9bbdf0c9da2516feee Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Thu, 10 Aug 2023 16:17:35 +0200 Subject: [PATCH 4/6] chore(docs): added documentation --- docs/core/event_handler/api_gateway.md | 10 ++++++++- .../src/strip_route_prefix_regex.py | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 examples/event_handler_rest/src/strip_route_prefix_regex.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 708a9de6855..dcfa38f6f9a 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -272,7 +272,7 @@ When using [Custom Domain API Mappings feature](https://docs.aws.amazon.com/apig **Scenario**: You have a custom domain `api.mydomain.dev`. Then you set `/payment` API Mapping to forward any payment requests to your Payments API. -**Challenge**: This means your `path` value for any API requests will always contain `/payment/`, 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"}. +**Challenge**: This means your `path` value for any API requests will always contain `/payment/`, 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"}. 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. @@ -293,6 +293,14 @@ To address this API Gateway behavior, we use `strip_prefixes` parameter to accou 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 `/`. +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). + +=== "strip_route_prefix_regex.py" + + ```python hl_lines="12" + --8<-- "examples/event_handler_rest/src/strip_route_prefix_regex.py" + ``` + ## Advanced ### CORS diff --git a/examples/event_handler_rest/src/strip_route_prefix_regex.py b/examples/event_handler_rest/src/strip_route_prefix_regex.py new file mode 100644 index 00000000000..4ea4b4249f4 --- /dev/null +++ b/examples/event_handler_rest/src/strip_route_prefix_regex.py @@ -0,0 +1,21 @@ +import re + +from aws_lambda_powertools.event_handler import APIGatewayRestResolver +from aws_lambda_powertools.utilities.typing import LambdaContext + +# This will support: +# /v1/dev/subscriptions/ +# /v1/stg/subscriptions/ +# /v1/qa/subscriptions/ +# /v2/dev/subscriptions/ +# ... +app = APIGatewayRestResolver(strip_prefixes=[re.compile(r"/v[1-3]+/(dev|stg|qa)")]) + + +@app.get("/subscriptions/") +def get_subscription(subscription): + return {"subscription_id": subscription} + + +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) From e60d3b37d944fd1bd88f19fd13b298cc22caed1c Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Fri, 11 Aug 2023 15:26:25 +0200 Subject: [PATCH 5/6] chore: reword --- aws_lambda_powertools/event_handler/api_gateway.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 72f280b3ea2..8de90407675 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -535,8 +535,9 @@ def __init__( serializer : Callable, optional function to serialize `obj` to a JSON formatted `str`, by default json.dumps strip_prefixes: List[Union[str, Pattern]], optional - optional list of prefixes to be removed from the request path before doing the routing. This is often used - with api gateways with multiple custom mappings. Each prefix can be a static string or a compiled regex + optional list of prefixes to be removed from the request path before doing the routing. + This is often used with api gateways with multiple custom mappings. + Each prefix can be a static string or a compiled regex pattern """ self._proxy_type = proxy_type self._dynamic_routes: List[Route] = [] From 5684ef1363a2038def9ff8bb08286bb049fccd06 Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Fri, 11 Aug 2023 15:36:23 +0200 Subject: [PATCH 6/6] fix: added tests --- aws_lambda_powertools/event_handler/api_gateway.py | 9 +++++---- tests/functional/event_handler/test_api_gateway.py | 12 ++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 8de90407675..1e6fe2a50bb 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -724,10 +724,11 @@ def _remove_prefix(self, path: str) -> str: if isinstance(prefix, Pattern): path = re.sub(prefix, "", path) - # When using regexes, we might get into a point where everything is removed - # from the string, so we check if it's empty and change it accordingly. - if not path: - path = "/" + # When using regexes, we might get into a point where everything is removed + # from the string, so we check if it's empty and return /, since there's nothing + # else to strip anymore. + if not path: + return "/" return path diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index f40bb2439ee..2afd1241bed 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -1098,6 +1098,18 @@ def foo(): assert response["statusCode"] == 200 +def test_empty_path_when_using_regexes(): + app = ApiGatewayResolver(strip_prefixes=[re.compile(r"/(dev|stg)")]) + + @app.get("/") + def foo(): + ... + + response = app({"httpMethod": "GET", "path": "/dev"}, None) + + assert response["statusCode"] == 200 + + @pytest.mark.parametrize( "prefix", [