From a8b3f4832c95ad3fb2f4b5dda42af3b07ea2e34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 3 Aug 2022 13:46:04 +0200 Subject: [PATCH 01/10] feat(event_sources): add support for Lambda Function URL events --- .../utilities/data_classes/__init__.py | 2 + .../data_classes/api_gateway_proxy_event.py | 19 ++++-- .../utilities/data_classes/common.py | 3 +- docs/utilities/data_classes.md | 15 +++++ tests/events/lambdaFunctionUrlEvent.json | 52 ++++++++++++++ .../data_classes/test_lambda_function_url.py | 67 +++++++++++++++++++ 6 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 tests/events/lambdaFunctionUrlEvent.json create mode 100644 tests/functional/data_classes/test_lambda_function_url.py diff --git a/aws_lambda_powertools/utilities/data_classes/__init__.py b/aws_lambda_powertools/utilities/data_classes/__init__.py index c5391880122..b20ec504732 100644 --- a/aws_lambda_powertools/utilities/data_classes/__init__.py +++ b/aws_lambda_powertools/utilities/data_classes/__init__.py @@ -12,6 +12,7 @@ from .event_bridge_event import EventBridgeEvent from .event_source import event_source from .kinesis_stream_event import KinesisStreamEvent +from .lambda_function_url_event import LambdaFunctionUrlEvent from .s3_event import S3Event from .ses_event import SESEvent from .sns_event import SNSEvent @@ -28,6 +29,7 @@ "DynamoDBStreamEvent", "EventBridgeEvent", "KinesisStreamEvent", + "LambdaFunctionUrlEvent", "S3Event", "SESEvent", "SNSEvent", diff --git a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py index adce2d4b11b..1fc66c42c3b 100644 --- a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py +++ b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py @@ -127,19 +127,22 @@ def caller_id(self) -> Optional[str]: def cognito_amr(self) -> Optional[List[str]]: """This represents how the user was authenticated. AMR stands for Authentication Methods References as per the openid spec""" - return self["cognitoIdentity"].get("amr") + cognito_identity = self["cognitoIdentity"] or {} + return cognito_identity.get("amr") @property def cognito_identity_id(self) -> Optional[str]: """The Amazon Cognito identity ID of the caller making the request. Available only if the request was signed with Amazon Cognito credentials.""" - return self["cognitoIdentity"].get("identityId") + cognito_identity = self.get("cognitoIdentity") or {} + return cognito_identity.get("identityId") @property def cognito_identity_pool_id(self) -> Optional[str]: """The Amazon Cognito identity pool ID of the caller making the request. Available only if the request was signed with Amazon Cognito credentials.""" - return self["cognitoIdentity"].get("identityPoolId") + cognito_identity = self.get("cognitoIdentity") or {} + return cognito_identity.get("identityPoolId") @property def principal_org_id(self) -> Optional[str]: @@ -159,12 +162,14 @@ def user_id(self) -> Optional[str]: class RequestContextV2Authorizer(DictWrapper): @property - def jwt_claim(self) -> Dict[str, Any]: - return self["jwt"]["claims"] + def jwt_claim(self) -> Optional[Dict[str, Any]]: + jwt = self.get("jwt") or {} + return jwt.get("claims") @property - def jwt_scopes(self) -> List[str]: - return self["jwt"]["scopes"] + def jwt_scopes(self) -> Optional[List[str]]: + jwt = self.get("jwt") or {} + return jwt.get("scopes") @property def get_lambda(self) -> Optional[Dict[str, Any]]: diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index c651c225fab..47d28491c46 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -395,5 +395,6 @@ def time_epoch(self) -> int: @property def authentication(self) -> Optional[RequestContextClientCert]: """Optional when using mutual TLS authentication""" - client_cert = self["requestContext"].get("authentication", {}).get("clientCert") + authentication = self["requestContext"].get("authentication") or {} + client_cert = authentication.get("clientCert") return None if client_cert is None else RequestContextClientCert(client_cert) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 8353d904bb1..2d1d32b4366 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -3,6 +3,8 @@ title: Event Source Data Classes description: Utility --- + + Event Source Data Classes utility provides classes self-describing Lambda event sources. ## Key Features @@ -73,6 +75,7 @@ Event Source | Data_class [DynamoDB streams](#dynamodb-streams) | `DynamoDBStreamEvent`, `DynamoDBRecordEventName` [EventBridge](#eventbridge) | `EventBridgeEvent` [Kinesis Data Stream](#kinesis-streams) | `KinesisStreamEvent` +[Lambda Function URL](#lambda-function-url) | `LambdaFunctionUrlEvent` [Rabbit MQ](#rabbit-mq) | `RabbitMQEvent` [S3](#s3) | `S3Event` [S3 Object Lambda](#s3-object-lambda) | `S3ObjectLambdaEvent` @@ -837,6 +840,18 @@ or plain text, depending on the original payload. do_something_with(data) ``` +### Lambda Function URL + +=== "app.py" + + ```python + from aws_lambda_powertools.utilities.data_classes import event_source, LambdaFunctoinUrlEvent + + @event_source(data_class=LambdaFunctionUrlEvent) + def lambda_handler(event: LambdaFunctionUrlEvent, context): + do_something_with(event.body) + ``` + ### Rabbit MQ It is used for [Rabbit MQ payloads](https://docs.aws.amazon.com/lambda/latest/dg/with-mq.html){target="_blank"}, also see diff --git a/tests/events/lambdaFunctionUrlEvent.json b/tests/events/lambdaFunctionUrlEvent.json new file mode 100644 index 00000000000..94e1fbba8cd --- /dev/null +++ b/tests/events/lambdaFunctionUrlEvent.json @@ -0,0 +1,52 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "header1": "value1", + "header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "", + "authentication": null, + "authorizer": { + "iam": { + "accessKey": "AKIA...", + "accountId": "111122223333", + "callerId": "AIDA...", + "cognitoIdentity": null, + "principalOrgId": null, + "userArn": "arn:aws:iam::111122223333:user/example-user", + "userId": "AIDA..." + } + }, + "domainName": ".lambda-url.us-west-2.on.aws", + "domainPrefix": "", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "123.123.123.123", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "Hello from client!", + "pathParameters": null, + "isBase64Encoded": false, + "stageVariables": null +} \ No newline at end of file diff --git a/tests/functional/data_classes/test_lambda_function_url.py b/tests/functional/data_classes/test_lambda_function_url.py new file mode 100644 index 00000000000..cbd4a64eb45 --- /dev/null +++ b/tests/functional/data_classes/test_lambda_function_url.py @@ -0,0 +1,67 @@ +from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent +from tests.functional.utils import load_event + + +def test_lambda_function_url_event(): + event = LambdaFunctionUrlEvent(load_event("lambdaFunctionUrlEvent.json")) + + assert event.version == "2.0" + assert event.route_key == "$default" + + assert event.path == "/my/path" + assert event.raw_query_string == "parameter1=value1¶meter1=value2¶meter2=value" + + cookies = event.cookies + assert len(cookies) == 2 + assert cookies[0] == "cookie1" + + headers = event.headers + assert len(headers) == 2 + + query_string_parameters = event.query_string_parameters + assert len(query_string_parameters) == 2 + assert query_string_parameters.get("parameter2") == "value" + + assert event.is_base64_encoded is False + assert event.body == "Hello from client!" + assert event.decoded_body == event.body + assert event.path_parameters is None + assert event.stage_variables is None + assert event.http_method == "POST" + + request_context = event.request_context + + assert request_context.account_id == "123456789012" + assert request_context.api_id is not None + assert request_context.domain_name == ".lambda-url.us-west-2.on.aws" + assert request_context.domain_prefix == "" + assert request_context.request_id == "id" + assert request_context.route_key == "$default" + assert request_context.stage == "$default" + assert request_context.time is not None + assert request_context.time_epoch == 1583348638390 + assert request_context.authentication is None + + http = request_context.http + assert http.method == "POST" + assert http.path == "/my/path" + assert http.protocol == "HTTP/1.1" + assert http.source_ip == "123.123.123.123" + assert http.user_agent == "agent" + + authorizer = request_context.authorizer + assert authorizer is not None + assert authorizer.jwt_claim is None + assert authorizer.jwt_scopes is None + assert authorizer.get_lambda is None + + iam = authorizer.iam + assert iam is not None + assert iam.access_key is not None + assert iam.account_id == "111122223333" + assert iam.caller_id is not None + assert iam.cognito_identity_id is None + assert iam.cognito_identity_pool_id is None + assert iam.principal_org_id is None + assert iam.user_id is not None + assert iam.user_arn == "arn:aws:iam::111122223333:user/example-user" From aca525c7d33d794901a33c1c8773f84ba3d99caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 3 Aug 2022 13:48:36 +0200 Subject: [PATCH 02/10] feat(event_handlers): add support for Lambda Function URL --- .../event_handler/__init__.py | 2 + .../event_handler/api_gateway.py | 11 ++++- .../event_handler/lambda_function_url.py | 18 +++++++++ .../logging/correlation_paths.py | 1 + .../data_classes/lambda_function_url_event.py | 19 +++++++++ .../event_handler/test_lambda_function_url.py | 40 +++++++++++++++++++ 6 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 aws_lambda_powertools/event_handler/lambda_function_url.py create mode 100644 aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py create mode 100644 tests/functional/event_handler/test_lambda_function_url.py diff --git a/aws_lambda_powertools/event_handler/__init__.py b/aws_lambda_powertools/event_handler/__init__.py index e7dc4608ee9..75d03d771e0 100644 --- a/aws_lambda_powertools/event_handler/__init__.py +++ b/aws_lambda_powertools/event_handler/__init__.py @@ -11,6 +11,7 @@ Response, ) from .appsync import AppSyncResolver +from .lambda_function_url import LambdaFunctionUrlResolver __all__ = [ "AppSyncResolver", @@ -19,5 +20,6 @@ "ALBResolver", "ApiGatewayResolver", "CORSConfig", + "LambdaFunctionUrlResolver", "Response", ] diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 78cee8f2051..903fc7e828f 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -17,7 +17,12 @@ from aws_lambda_powertools.shared import constants from aws_lambda_powertools.shared.functions import resolve_truthy_env_var_choice from aws_lambda_powertools.shared.json_encoder import Encoder -from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2 +from aws_lambda_powertools.utilities.data_classes import ( + ALBEvent, + APIGatewayProxyEvent, + APIGatewayProxyEventV2, + LambdaFunctionUrlEvent, +) from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent from aws_lambda_powertools.utilities.typing import LambdaContext @@ -36,6 +41,7 @@ class ProxyEventType(Enum): APIGatewayProxyEvent = "APIGatewayProxyEvent" APIGatewayProxyEventV2 = "APIGatewayProxyEventV2" ALBEvent = "ALBEvent" + LambdaFunctionUrlEvent = "LambdaFunctionUrlEvent" class CORSConfig: @@ -546,6 +552,9 @@ def _to_proxy_event(self, event: Dict) -> BaseProxyEvent: if self._proxy_type == ProxyEventType.APIGatewayProxyEventV2: logger.debug("Converting event to API Gateway HTTP API contract") return APIGatewayProxyEventV2(event) + if self._proxy_type == ProxyEventType.LambdaFunctionUrlEvent: + logger.debug("Converting event to Lambda Function URL contract") + return LambdaFunctionUrlEvent(event) logger.debug("Converting event to ALB contract") return ALBEvent(event) diff --git a/aws_lambda_powertools/event_handler/lambda_function_url.py b/aws_lambda_powertools/event_handler/lambda_function_url.py new file mode 100644 index 00000000000..592a662f3be --- /dev/null +++ b/aws_lambda_powertools/event_handler/lambda_function_url.py @@ -0,0 +1,18 @@ +from typing import Callable, Dict, List, Optional + +from aws_lambda_powertools.event_handler import CORSConfig +from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, ProxyEventType +from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent + + +class LambdaFunctionUrlResolver(ApiGatewayResolver): + current_event: LambdaFunctionUrlEvent + + def __init__( + self, + cors: Optional[CORSConfig] = None, + debug: Optional[bool] = None, + serializer: Optional[Callable[[Dict], str]] = None, + strip_prefixes: Optional[List[str]] = None, + ): + super().__init__(ProxyEventType.LambdaFunctionUrlEvent, cors, debug, serializer, strip_prefixes) diff --git a/aws_lambda_powertools/logging/correlation_paths.py b/aws_lambda_powertools/logging/correlation_paths.py index b6926f08591..01de17eddcb 100644 --- a/aws_lambda_powertools/logging/correlation_paths.py +++ b/aws_lambda_powertools/logging/correlation_paths.py @@ -6,4 +6,5 @@ APPSYNC_RESOLVER = 'request.headers."x-amzn-trace-id"' APPLICATION_LOAD_BALANCER = 'headers."x-amzn-trace-id"' EVENT_BRIDGE = "id" +LAMBDA_FUNCTION_URL = "requestContext.requestId" S3_OBJECT_LAMBDA = "xAmzRequestId" diff --git a/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py b/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py new file mode 100644 index 00000000000..e86fd55a1fe --- /dev/null +++ b/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py @@ -0,0 +1,19 @@ +from aws_lambda_powertools.utilities.data_classes.api_gateway_proxy_event import APIGatewayProxyEventV2 + + +class LambdaFunctionUrlEvent(APIGatewayProxyEventV2): + """AWS Lambda Function URL event + + Notes: + ----- + For now, this seems to follow the exact payload as HTTP APIs Payload Format Version 2.0. + Certain keys in this payload format don't make sense for function urls (e.g: `routeKey`). + These keys will always be null. + + Documentation: + - https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + - https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html + + """ + + pass diff --git a/tests/functional/event_handler/test_lambda_function_url.py b/tests/functional/event_handler/test_lambda_function_url.py new file mode 100644 index 00000000000..e4831754856 --- /dev/null +++ b/tests/functional/event_handler/test_lambda_function_url.py @@ -0,0 +1,40 @@ +from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver, Response, content_types +from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent +from tests.functional.utils import load_event + + +def test_lambda_function_url_event(): + # GIVEN a Lambda Function Url type event + app = LambdaFunctionUrlResolver() + + @app.post("/my/path") + def foo(): + assert isinstance(app.current_event, LambdaFunctionUrlEvent) + assert app.lambda_context == {} + assert app.current_event.request_context.stage is not None + return Response(200, content_types.TEXT_HTML, "foo") + + # WHEN calling the event handler + result = app(load_event("lambdaFunctionUrlEvent.json"), {}) + + # THEN process event correctly + # AND set the current_event type as LambdaFunctionUrlEvent + assert result["statusCode"] == 200 + assert result["headers"]["Content-Type"] == content_types.TEXT_HTML + assert result["body"] == "foo" + + +def test_lambda_function_url_no_matches(): + # GIVEN a Lambda Function Url type event + app = LambdaFunctionUrlResolver() + + @app.post("/no_match") + def foo(): + raise RuntimeError() + + # WHEN calling the event handler + result = app(load_event("lambdaFunctionUrlEvent.json"), {}) + + # THEN process event correctly + # AND return 404 because the event doesn't match any known route + assert result["statusCode"] == 404 From cb3bc5b462460e9cbf0cc7d54f824b97fe6eaf2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 3 Aug 2022 13:49:12 +0200 Subject: [PATCH 03/10] chore(docs): add documentation for Lambda Functoin URLs --- docs/core/event_handler/api_gateway.md | 47 +- .../core/event_handler/lambda_function_url.md | 544 ++++++++++++++++++ docs/index.md | 32 +- .../sam/template.yaml | 31 + ..._started_lambda_function_url_resolver.json | 51 ++ ...ng_started_lambda_function_url_resolver.py | 28 + 6 files changed, 706 insertions(+), 27 deletions(-) create mode 100644 docs/core/event_handler/lambda_function_url.md create mode 100644 examples/event_handler_lambda_function_url/sam/template.yaml create mode 100644 examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.json create mode 100644 examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.py diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 707d9687e63..1f61039f195 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -3,11 +3,11 @@ title: REST API description: Core utility --- -Event handler for Amazon API Gateway REST and HTTP APIs, and Application Loader Balancer (ALB). +Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Balancer (ALB), and Lambda Function URLs. ## Key Features -* Lightweight routing to reduce boilerplate for API Gateway REST/HTTP API and ALB +* Lightweight routing to reduce boilerplate for API Gateway REST/HTTP API, ALB and Lambda Function URLs. * Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer * Built-in integration with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"} for self-documented event schema @@ -18,15 +18,23 @@ Event handler for Amazon API Gateway REST and HTTP APIs, and Application Loader ### Required resources -You must have an existing [API Gateway Proxy integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html){target="_blank"} or [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html){target="_blank"} configured to invoke your Lambda function. +If you're using any API Gateway integration, you must have an existing [API Gateway Proxy integration](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html){target="_blank"} or [ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html){target="_blank"} configured to invoke your Lambda function. -This is the sample infrastructure for API Gateway we are using for the examples in this documentation. +This is the sample infrastructure for API Gateway and Lambda Function URLs we are using for the examples in this documentation. ???+ info "There is no additional permissions or dependencies required to use this utility." -```yaml title="AWS Serverless Application Model (SAM) example" ---8<-- "examples/event_handler_rest/sam/template.yaml" -``` +=== "API Gateway SAM Template" + + ```yaml title="AWS Serverless Application Model (SAM) example" + --8<-- "examples/event_handler_rest/sam/template.yaml" + ``` + +=== "Lambda Function URL SAM Template" + + ```yaml title="AWS Serverless Application Model (SAM) example" + --8<-- "examples/event_handler_lambda_function_url/sam/template.yaml" + ``` ### Event Resolvers @@ -34,7 +42,7 @@ Before you decorate your functions to handle a given path and HTTP method(s), yo A resolver will handle request resolution, including [one or more routers](#split-routes-with-router), and give you access to the current event via typed properties. -For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, and `ALBResolver`. +For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, `ALBResolver`, and `LambdaFunctionUrlResolver` . ???+ info We will use `APIGatewayRestResolver` as the default across examples. @@ -87,6 +95,22 @@ When using Amazon Application Load Balancer (ALB) to front your Lambda functions --8<-- "examples/event_handler_rest/src/getting_started_alb_api_resolver.py" ``` +#### Lambda Function URL + +When using an [AWS Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), you can use `LambdaFunctionUrlResolver`. + +=== "getting_started_lambda_function_url_resolver.py" + + ```python hl_lines="5 11" title="Using Lambda Function URL resolver" + --8<-- "examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.py" + ``` + +=== "getting_started_lambda_function_url_resolver.json" + + ```json hl_lines="4-5" title="Example payload delivered to the handler" + --8<-- "examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.json" + ``` + ### Dynamic routes You can use `/todos/` to configure dynamic URL paths, where `` will be resolved at runtime. @@ -270,7 +294,7 @@ This will ensure that CORS headers are always returned as part of the response w #### Pre-flight -Pre-flight (OPTIONS) calls are typically handled at the API Gateway level as per [our sample infrastructure](#required-resources), no Lambda integration necessary. However, ALB expects you to handle pre-flight requests. +Pre-flight (OPTIONS) calls are typically handled at the API Gateway or Lambda Function URL level as per [our sample infrastructure](#required-resources), no Lambda integration necessary. However, ALB expects you to handle pre-flight requests. For convenience, we automatically handle that for you as long as you [setup CORS in the constructor level](#cors). @@ -338,6 +362,7 @@ Like `compress` feature, the client must send the `Accept` header with the corre ???+ warning This feature requires API Gateway to configure binary media types, see [our sample infrastructure](#required-resources) for reference. + For Lambda Function URLs, no additional configuration is necessary. === "binary_responses.py" @@ -380,7 +405,7 @@ This will enable full tracebacks errors in the response, print request and respo ### Custom serializer -You can instruct API Gateway handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. +You can instruct an event handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. ```python hl_lines="35 40" title="Using a custom JSON serializer for responses" --8<-- "examples/event_handler_rest/src/custom_serializer.py" @@ -501,7 +526,7 @@ A micro function means that your final code artifact will be different to each f **Downsides** -* **Upfront investment**. You need custom build tooling to bundle assets, including [C bindings for runtime compatibility](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html){target="_blank"}. `Operations become more elaborate — you need to standardize tracing labels/annotations, structured logging, and metrics to pinpoint root causes. +* **Upfront investment**. You need custom build tooling to bundle assets, including [C bindings for runtime compatibility](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html){target="_blank"}. Operations become more elaborate — you need to standardize tracing labels/annotations, structured logging, and metrics to pinpoint root causes. * Engineering discipline is necessary for both approaches. Micro-function approach however requires further attention in consistency as the number of functions grow, just like any distributed system. * **Harder to share code**. Shared code must be carefully evaluated to avoid unnecessary deployments when that changes. Equally, if shared code isn't a library, your development, building, deployment tooling need to accommodate the distinct layout. diff --git a/docs/core/event_handler/lambda_function_url.md b/docs/core/event_handler/lambda_function_url.md new file mode 100644 index 00000000000..64df750deeb --- /dev/null +++ b/docs/core/event_handler/lambda_function_url.md @@ -0,0 +1,544 @@ +--- +title: Lambda Function URL +description: Core utility +--- + +Event handler for AWS Lambda Function URLs. + +## Key Features + +* Lightweight routing to reduce boilerplate for Lambda Function URLs +* Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer +* Built-in integration with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"} for self-documented event schema + +## Getting started + +???+ tip + All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. + +### Required resources + +You must have an existing AWS Lambda Function with a [configured Function URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html). + +This is the sample infrastructure for AWS Lambda we are using for the examples in this documentation. + +???+ info "There is no additional permissions or dependencies required to use this utility." + +```yaml title="AWS Serverless Application Model (SAM) example" +--8<-- "examples/event_handler_lambda_function_url/sam/template.yaml" +``` + +### Event Resolvers + +Before you decorate your functions to handle a given path and HTTP method(s), you need to initialize a resolver. + +A resolver will handle request resolution, including [one or more routers](#split-routes-with-router), and give you access to the current event via typed properties. + +For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, and `ALBResolver`. + +???+ info + We will use `APIGatewayRestResolver` as the default across examples. + +#### API Gateway REST API + +When using Amazon API Gateway REST API to front your Lambda functions, you can use `APIGatewayRestResolver`. + +Here's an example on how we can handle the `/todos` path. + +???+ info + We automatically serialize `Dict` responses as JSON, trim whitespace for compact responses, and set content-type to `application/json`. + +=== "getting_started_rest_api_resolver.py" + + ```python hl_lines="5 11 14 28" + --8<-- "examples/event_handler_rest/src/getting_started_rest_api_resolver.py" + ``` + +=== "getting_started_rest_api_resolver.json" + + This utility uses `path` and `httpMethod` to route to the right function. This helps make unit tests and local invocation easier too. + + ```json hl_lines="4-5" + --8<-- "examples/event_handler_rest/src/getting_started_rest_api_resolver.json" + ``` + +=== "getting_started_rest_api_resolver_output.json" + + ```json + --8<-- "examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json" + ``` + +#### API Gateway HTTP API + +When using Amazon API Gateway HTTP API to front your Lambda functions, you can use `APIGatewayHttpResolver`. + +???+ note + Using HTTP API v1 payload? Use `APIGatewayRestResolver` instead. `APIGatewayHttpResolver` defaults to v2 payload. + +```python hl_lines="5 11" title="Using HTTP API resolver" +--8<-- "examples/event_handler_rest/src/getting_started_http_api_resolver.py" +``` + +#### Application Load Balancer + +When using Amazon Application Load Balancer (ALB) to front your Lambda functions, you can use `ALBResolver`. + +```python hl_lines="5 11" title="Using ALB resolver" +--8<-- "examples/event_handler_rest/src/getting_started_alb_api_resolver.py" +``` + +### Dynamic routes + +You can use `/todos/` to configure dynamic URL paths, where `` will be resolved at runtime. + +Each dynamic route you set must be part of your function signature. This allows us to call your function using keyword arguments when matching your dynamic route. + +???+ note + For brevity, we will only include the necessary keys for each sample request for the example to work. + +=== "dynamic_routes.py" + + ```python hl_lines="14 16" + --8<-- "examples/event_handler_rest/src/dynamic_routes.py" + ``` + +=== "dynamic_routes.json" + + ```json + --8<-- "examples/event_handler_rest/src/dynamic_routes.json" + ``` + +???+ tip + You can also nest dynamic paths, for example `/todos//`. + +???+ tip + You can also nest dynamic paths, for example `/todos//`. + +#### Catch-all routes + +???+ note + We recommend having explicit routes whenever possible; use catch-all routes sparingly. + +You can use a [regex](https://docs.python.org/3/library/re.html#regular-expression-syntax){target="_blank"} string to handle an arbitrary number of paths within a request, for example `.+`. + +You can also combine nested paths with greedy regex to catch in between routes. + +???+ warning + We choose the most explicit registered route that matches an incoming event. + +=== "dynamic_routes_catch_all.py" + + ```python hl_lines="11" + --8<-- "examples/event_handler_rest/src/dynamic_routes_catch_all.py" + ``` + +=== "dynamic_routes_catch_all.json" + + ```json + --8<-- "examples/event_handler_rest/src/dynamic_routes_catch_all.json" + ``` + +### HTTP Methods + +You can use named decorators to specify the HTTP method that should be handled in your functions. That is, `app.`, where the HTTP method could be `get`, `post`, `put`, `patch`, `delete`, and `options`. + +=== "http_methods.py" + + ```python hl_lines="14 17" + --8<-- "examples/event_handler_rest/src/http_methods.py" + ``` + +=== "http_methods.json" + + ```json + --8<-- "examples/event_handler_rest/src/http_methods.json" + ``` + +If you need to accept multiple HTTP methods in a single function, you can use the `route` method and pass a list of HTTP methods. + +```python hl_lines="15" title="Handling multiple HTTP Methods" +--8<-- "examples/event_handler_rest/src/http_methods_multiple.py" +``` + +???+ note + It is generally better to have separate functions for each HTTP method, as the functionality tends to differ depending on which method is used. + +### Accessing request details + +Event Handler integrates with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"}, and it exposes their respective resolver request details and convenient methods under `app.current_event`. + +That is why you see `app.resolve(event, context)` in every example. This allows Event Handler to resolve requests, and expose data like `app.lambda_context` and `app.current_event`. + +#### Query strings and payload + +Within `app.current_event` property, you can access all available query strings as a dictionary via `query_string_parameters`, or a specific one via `get_query_string_value` method. + +You can access the raw payload via `body` property, or if it's a JSON string you can quickly deserialize it via `json_body` property - like the earlier example in the [HTTP Methods](#http-methods) section. + +```python hl_lines="19 24" title="Accessing query strings and raw payload" +--8<-- "examples/event_handler_rest/src/accessing_request_details.py" +``` + +#### Headers + +Similarly to [Query strings](#query-strings-and-payload), you can access headers as dictionary via `app.current_event.headers`, or by name via `get_header_value`. + +```python hl_lines="19" title="Accessing HTTP Headers" +--8<-- "examples/event_handler_rest/src/accessing_request_details_headers.py" +``` + +### Handling not found routes + +By default, we return `404` for any unmatched route. + +You can use **`not_found`** decorator to override this behavior, and return a custom **`Response`**. + +```python hl_lines="14 18" title="Handling not found" +--8<-- "examples/event_handler_rest/src/not_found_routes.py" +``` + +### Exception handling + +You can use **`exception_handler`** decorator with any Python exception. This allows you to handle a common exception outside your route, for example validation errors. + +```python hl_lines="13-14" title="Exception handling" +--8<-- "examples/event_handler_rest/src/exception_handling.py" +``` + +### Raising HTTP errors + +You can easily raise any HTTP Error back to the client using `ServiceError` exception. This ensures your Lambda function doesn't fail but return the correct HTTP response signalling the error. + +???+ info + If you need to send custom headers, use [Response](#fine-grained-responses) class instead. + +We provide pre-defined errors for the most popular ones such as HTTP 400, 401, 404, 500. + +```python hl_lines="6-11 23 28 33 38 43" title="Raising common HTTP Status errors (4xx, 5xx)" +--8<-- "examples/event_handler_rest/src/raising_http_errors.py" +``` + +### Custom Domain API Mappings + +When using [Custom Domain API Mappings feature](https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-mappings.html){target="_blank"}, you must use **`strip_prefixes`** param in the `APIGatewayRestResolver` constructor. + +**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/awslabs/aws-lambda-powertools-roadmap/issues/34). + +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. + +=== "custom_api_mapping.py" + + ```python hl_lines="8" + --8<-- "examples/event_handler_rest/src/custom_api_mapping.py" + ``` + +=== "custom_api_mapping.json" + + ```json + --8<-- "examples/event_handler_rest/src/custom_api_mapping.json" + ``` + +???+ note + After removing a path prefix with `strip_prefixes`, the new root path will automatically be mapped to the path argument of `/`. + + 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 `/`. + +## Advanced + +### CORS + +You can configure CORS at the `APIGatewayRestResolver` constructor via `cors` parameter using the `CORSConfig` class. + +This will ensure that CORS headers are always returned as part of the response when your functions match the path invoked. + +???+ tip + Optionally disable CORS on a per path basis with `cors=False` parameter. + +=== "setting_cors.py" + + ```python hl_lines="5 11-12 34" + --8<-- "examples/event_handler_rest/src/setting_cors.py" + ``` + +=== "setting_cors_output.json" + + ```json + --8<-- "examples/event_handler_rest/src/setting_cors_output.json" + ``` + +#### Pre-flight + +Pre-flight (OPTIONS) calls are typically handled at the API Gateway level as per [our sample infrastructure](#required-resources), no Lambda integration necessary. However, ALB expects you to handle pre-flight requests. + +For convenience, we automatically handle that for you as long as you [setup CORS in the constructor level](#cors). + +#### Defaults + +For convenience, these are the default values when using `CORSConfig` to enable CORS: + +???+ warning + Always configure `allow_origin` when using in production. + +| Key | Value | Note | +| -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **[allow_origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin){target="_blank"}**: `str` | `*` | Only use the default value for development. **Never use `*` for production** unless your use case requires it | +| **[allow_headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers){target="_blank"}**: `List[str]` | `[Authorization, Content-Type, X-Amz-Date, X-Api-Key, X-Amz-Security-Token]` | Additional headers will be appended to the default list for your convenience | +| **[expose_headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers){target="_blank"}**: `List[str]` | `[]` | Any additional header beyond the [safe listed by CORS specification](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header){target="_blank"}. | +| **[max_age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age){target="_blank"}**: `int` | `` | Only for pre-flight requests if you choose to have your function to handle it instead of API Gateway | +| **[allow_credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials){target="_blank"}**: `bool` | `False` | Only necessary when you need to expose cookies, authorization headers or TLS client certificates. | + +### Fine grained responses + +You can use the `Response` class to have full control over the response, for example you might want to add additional headers or set a custom Content-type. + +=== "fine_grained_responses.py" + + ```python hl_lines="7 24-28" + --8<-- "examples/event_handler_rest/src/fine_grained_responses.py" + ``` + +=== "fine_grained_responses_output.json" + + ```json + --8<-- "examples/event_handler_rest/src/fine_grained_responses_output.json" + ``` + +### Compress + +You can compress with gzip and base64 encode your responses via `compress` parameter. + +???+ warning + The client must send the `Accept-Encoding` header, otherwise a normal response will be sent. + +=== "compressing_responses.py" + + ```python hl_lines="14" + --8<-- "examples/event_handler_rest/src/compressing_responses.py" + ``` + +=== "compressing_responses.json" + + ```json + --8<-- "examples/event_handler_rest/src/compressing_responses.json" + ``` + +=== "compressing_responses_output.json" + + ```json + --8<-- "examples/event_handler_rest/src/compressing_responses_output.json" + ``` + +### Binary responses + +For convenience, we automatically base64 encode binary responses. You can also use in combination with `compress` parameter if your client supports gzip. + +Like `compress` feature, the client must send the `Accept` header with the correct media type. + +???+ warning + This feature requires API Gateway to configure binary media types, see [our sample infrastructure](#required-resources) for reference. + +=== "binary_responses.py" + + ```python hl_lines="14 20" + --8<-- "examples/event_handler_rest/src/binary_responses.py" + ``` + +=== "binary_responses_logo.svg" + + ```xml + --8<-- "examples/event_handler_rest/src/binary_responses_logo.svg" + ``` + +=== "binary_responses.json" + + ```json + --8<-- "examples/event_handler_rest/src/binary_responses.json" + ``` + +=== "binary_responses_output.json" + + ```json + --8<-- "examples/event_handler_rest/src/binary_responses_output.json" + ``` + +### Debug mode + +You can enable debug mode via `debug` param, or via `POWERTOOLS_EVENT_HANDLER_DEBUG` [environment variable](../../index.md#environment-variables). + +This will enable full tracebacks errors in the response, print request and responses, and set CORS in development mode. + +???+ danger + This might reveal sensitive information in your logs and relax CORS restrictions, use it sparingly. + + It's best to use for local development only! + +```python hl_lines="11" title="Enabling debug mode" +--8<-- "examples/event_handler_rest/src/debug_mode.py" +``` + +### Custom serializer + +You can instruct API Gateway handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. + +```python hl_lines="35 40" title="Using a custom JSON serializer for responses" +--8<-- "examples/event_handler_rest/src/custom_serializer.py" +``` + +### Split routes with Router + +As you grow the number of routes a given Lambda function should handle, it is natural to split routes into separate files to ease maintenance - That's where the `Router` feature is useful. + +Let's assume you have `split_route.py` as your Lambda function entrypoint and routes in `split_route_module.py`. This is how you'd use the `Router` feature. + +=== "split_route_module.py" + + We import **Router** instead of **APIGatewayRestResolver**; syntax wise is exactly the same. + + ```python hl_lines="5 13 16 25 28" + --8<-- "examples/event_handler_rest/src/split_route_module.py" + ``` + +=== "split_route.py" + + We use `include_router` method and include all user routers registered in the `router` global object. + + ```python hl_lines="11" + --8<-- "examples/event_handler_rest/src/split_route.py" + ``` + +#### Route prefix + +In the previous example, `split_route_module.py` routes had a `/todos` prefix. This might grow over time and become repetitive. + +When necessary, you can set a prefix when including a router object. This means you could remove `/todos` prefix altogether. + +=== "split_route_prefix.py" + + ```python hl_lines="12" + --8<-- "examples/event_handler_rest/src/split_route_prefix.py" + ``` + +=== "split_route_prefix_module.py" + + ```python hl_lines="13 25" + --8<-- "examples/event_handler_rest/src/split_route_prefix_module.py" + ``` + +#### Sample layout + +This is a sample project layout for a monolithic function with routes split in different files (`/todos`, `/health`). + +```shell hl_lines="4 7 10 12-13" title="Sample project layout" +. +├── pyproject.toml # project app & dev dependencies; poetry, pipenv, etc. +├── poetry.lock +├── src +│ ├── __init__.py +│ ├── requirements.txt # sam build detect it automatically due to CodeUri: src. poetry export --format src/requirements.txt +│ └── todos +│ ├── __init__.py +│ ├── main.py # this will be our todos Lambda fn; it could be split in folders if we want separate fns same code base +│ └── routers # routers module +│ ├── __init__.py +│ ├── health.py # /health routes. from routers import todos; health.router +│ └── todos.py # /todos routes. from .routers import todos; todos.router +├── template.yml # SAM. CodeUri: src, Handler: todos.main.lambda_handler +└── tests + ├── __init__.py + ├── unit + │ ├── __init__.py + │ └── test_todos.py # unit tests for the todos router + │ └── test_health.py # unit tests for the health router + └── functional + ├── __init__.py + ├── conftest.py # pytest fixtures for the functional tests + └── test_main.py # functional tests for the main lambda handler +``` + +### Considerations + +This utility is optimized for fast startup, minimal feature set, and to quickly on-board customers familiar with frameworks like Flask — it's not meant to be a fully fledged framework. + +Event Handler naturally leads to a single Lambda function handling multiple routes for a given service, which can be eventually broken into multiple functions. + +Both single (monolithic) and multiple functions (micro) offer different set of trade-offs worth knowing. + +???+ tip + TL;DR. Start with a monolithic function, add additional functions with new handlers, and possibly break into micro functions if necessary. + +#### Monolithic function + +![Monolithic function sample](./../../media/monolithic-function.png) + +A monolithic function means that your final code artifact will be deployed to a single function. This is generally the best approach to start. + +_**Benefits**_ + +* **Code reuse**. It's easier to reason about your service, modularize it and reuse code as it grows. Eventually, it can be turned into a standalone library. +* **No custom tooling**. Monolithic functions are treated just like normal Python packages; no upfront investment in tooling. +* **Faster deployment and debugging**. Whether you use all-at-once, linear, or canary deployments, a monolithic function is a single deployable unit. IDEs like PyCharm and VSCode have tooling to quickly profile, visualize, and step through debug any Python package. + +_**Downsides**_ + +* **Cold starts**. Frequent deployments and/or high load can diminish the benefit of monolithic functions depending on your latency requirements, due to [Lambda scaling model](https://docs.aws.amazon.com/lambda/latest/dg/invocation-scaling.html){target="_blank"}. Always load test to pragmatically balance between your customer experience and development cognitive load. +* **Granular security permissions**. The micro function approach enables you to use fine-grained permissions & access controls, separate external dependencies & code signing at the function level. Conversely, you could have multiple functions while duplicating the final code artifact in a monolithic approach. + * Regardless, least privilege can be applied to either approaches. +* **Higher risk per deployment**. A misconfiguration or invalid import can cause disruption if not caught earlier in automated testing. Multiple functions can mitigate misconfigurations but they would still share the same code artifact. You can further minimize risks with multiple environments in your CI/CD pipeline. + +#### Micro function + +![Micro function sample](./../../media/micro-function.png) + +A micro function means that your final code artifact will be different to each function deployed. This is generally the approach to start if you're looking for fine-grain control and/or high load on certain parts of your service. + +**Benefits** + +* **Granular scaling**. A micro function can benefit from the [Lambda scaling model](https://docs.aws.amazon.com/lambda/latest/dg/invocation-scaling.html){target="_blank"} to scale differently depending on each part of your application. Concurrency controls and provisioned concurrency can also be used at a granular level for capacity management. +* **Discoverability**. Micro functions are easier do visualize when using distributed tracing. Their high-level architectures can be self-explanatory, and complexity is highly visible — assuming each function is named to the business purpose it serves. +* **Package size**. An independent function can be significant smaller (KB vs MB) depending on external dependencies it require to perform its purpose. Conversely, a monolithic approach can benefit from [Lambda Layers](https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html){target="_blank"} to optimize builds for external dependencies. + +**Downsides** + +* **Upfront investment**. You need custom build tooling to bundle assets, including [C bindings for runtime compatibility](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html){target="_blank"}. `Operations become more elaborate — you need to standardize tracing labels/annotations, structured logging, and metrics to pinpoint root causes. + * Engineering discipline is necessary for both approaches. Micro-function approach however requires further attention in consistency as the number of functions grow, just like any distributed system. +* **Harder to share code**. Shared code must be carefully evaluated to avoid unnecessary deployments when that changes. Equally, if shared code isn't a library, +your development, building, deployment tooling need to accommodate the distinct layout. +* **Slower safe deployments**. Safely deploying multiple functions require coordination — AWS CodeDeploy deploys and verifies each function sequentially. This increases lead time substantially (minutes to hours) depending on the deployment strategy you choose. You can mitigate it by selectively enabling it in prod-like environments only, and where the risk profile is applicable. + * Automated testing, operational and security reviews are essential to stability in either approaches. + +## Testing your code + +You can test your routes by passing a proxy event request where `path` and `httpMethod`. + +=== "assert_http_response.py" + + ```python hl_lines="21-24" + --8<-- "examples/event_handler_rest/src/assert_http_response.py" + ``` + +=== "assert_http_response_module.py" + + ```python + --8<-- "examples/event_handler_rest/src/assert_http_response_module.py" + ``` + +## FAQ + +**What's the difference between this utility and frameworks like Chalice?** + +Chalice is a full featured microframework that manages application and infrastructure. This utility, however, is largely focused on routing to reduce boilerplate and expects you to setup and manage infrastructure with your framework of choice. + +That said, [Chalice has native integration with Lambda Powertools](https://aws.github.io/chalice/topics/middleware.html){target="_blank"} if you're looking for a more opinionated and web framework feature set. + +**What happened to `ApiGatewayResolver`?** + +It's been superseded by more explicit resolvers like `APIGatewayRestResolver`, `APIGatewayHttpResolver`, and `ALBResolver`. + +`ApiGatewayResolver` handled multiple types of event resolvers for convenience via `proxy_type` param. However, +it made it impossible for static checkers like Mypy and IDEs IntelliSense to know what properties a `current_event` would have due to late bound resolution. + +This provided a suboptimal experience for customers not being able to find all properties available besides common ones between API Gateway REST, HTTP, and ALB - while manually annotating `app.current_event` would work it is not the experience we want to provide to customers. + +`ApiGatewayResolver` will be deprecated in v2 and have appropriate warnings as soon as we have a v2 draft. diff --git a/docs/index.md b/docs/index.md index b9ae8349c9f..a270db26f42 100644 --- a/docs/index.md +++ b/docs/index.md @@ -411,22 +411,22 @@ sam init --location https://github.com/aws-samples/cookiecutter-aws-sam-python Core utilities such as Tracing, Logging, Metrics, and Event Handler will be available across all Lambda Powertools languages. Additional utilities are subjective to each language ecosystem and customer demand. -| Utility | Description | -| ----------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Tracing](./core/tracer.md) | Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions | -| [Logger](./core/logger.md) | Structured logging made easier, and decorator to enrich structured logging with key Lambda context details | -| [Metrics](./core/metrics.md) | Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) | -| [Event handler: AppSync](./core/event_handler/appsync.md) | AppSync event handler for Lambda Direct Resolver and Amplify GraphQL Transformer function | -| [Event handler: API Gateway and ALB](https://awslabs.github.io/aws-lambda-powertools-python/latest/core/event_handler/api_gateway/) | Amazon API Gateway REST/HTTP API and ALB event handler for Lambda functions invoked using Proxy integration | -| [Middleware factory](./utilities/middleware_factory.md) | Decorator factory to create your own middleware to run logic before, and after each Lambda invocation | -| [Parameters](./utilities/parameters.md) | Retrieve parameter values from AWS Systems Manager Parameter Store, AWS Secrets Manager, or Amazon DynamoDB, and cache them for a specific amount of time | -| [Batch processing](./utilities/batch.md) | Handle partial failures for AWS SQS batch processing | -| [Typing](./utilities/typing.md) | Static typing classes to speedup development in your IDE | -| [Validation](./utilities/validation.md) | JSON Schema validator for inbound events and responses | -| [Event source data classes](./utilities/data_classes.md) | Data classes describing the schema of common Lambda event triggers | -| [Parser](./utilities/parser.md) | Data parsing and deep validation using Pydantic | -| [Idempotency](./utilities/idempotency.md) | Idempotent Lambda handler | -| [Feature Flags](./utilities/feature_flags.md) | A simple rule engine to evaluate when one or multiple features should be enabled depending on the input | +| Utility | Description | +|----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Tracing](./core/tracer.md) | Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions | +| [Logger](./core/logger.md) | Structured logging made easier, and decorator to enrich structured logging with key Lambda context details | +| [Metrics](./core/metrics.md) | Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) | +| [Event handler: AppSync](./core/event_handler/appsync.md) | AppSync event handler for Lambda Direct Resolver and Amplify GraphQL Transformer function | +| [Event handler: API Gateway, ALB and Lambda Function URL](https://awslabs.github.io/aws-lambda-powertools-python/latest/core/event_handler/api_gateway/) | Amazon API Gateway REST/HTTP API and ALB event handler for Lambda functions invoked using Proxy integration | +| [Middleware factory](./utilities/middleware_factory.md) | Decorator factory to create your own middleware to run logic before, and after each Lambda invocation | +| [Parameters](./utilities/parameters.md) | Retrieve parameter values from AWS Systems Manager Parameter Store, AWS Secrets Manager, or Amazon DynamoDB, and cache them for a specific amount of time | +| [Batch processing](./utilities/batch.md) | Handle partial failures for AWS SQS batch processing | +| [Typing](./utilities/typing.md) | Static typing classes to speedup development in your IDE | +| [Validation](./utilities/validation.md) | JSON Schema validator for inbound events and responses | +| [Event source data classes](./utilities/data_classes.md) | Data classes describing the schema of common Lambda event triggers | +| [Parser](./utilities/parser.md) | Data parsing and deep validation using Pydantic | +| [Idempotency](./utilities/idempotency.md) | Idempotent Lambda handler | +| [Feature Flags](./utilities/feature_flags.md) | A simple rule engine to evaluate when one or multiple features should be enabled depending on the input | ## Environment variables diff --git a/examples/event_handler_lambda_function_url/sam/template.yaml b/examples/event_handler_lambda_function_url/sam/template.yaml new file mode 100644 index 00000000000..34b7f053263 --- /dev/null +++ b/examples/event_handler_lambda_function_url/sam/template.yaml @@ -0,0 +1,31 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 +Description: Hello world event handler Lambda Function URL + +Globals: + Function: + Timeout: 5 + Runtime: python3.9 + Tracing: Active + Environment: + Variables: + LOG_LEVEL: INFO + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_SERVICE_NAME: example + FunctionUrlConfig: + Cors: # see CORS section + # Notice that values here are Lists of Strings, vs comma-separated values on API Gateway + AllowOrigins: ["https://example.com"] + AllowHeaders: ["Content-Type", "Authorization", "X-Amz-Date"] + MaxAge: 300 + +Resources: + ApiFunction: + Type: AWS::Serverless::Function + Properties: + Handler: getting_started_lambda_function_url_resolver.lambda_handler + CodeUri: ../src + Description: API handler function + FunctionUrlConfig: + AuthType: NONE diff --git a/examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.json b/examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.json new file mode 100644 index 00000000000..1fd11605701 --- /dev/null +++ b/examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.json @@ -0,0 +1,51 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/todos", + "rawQueryString": "", + "headers": { + "x-amz-content-sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "x-amzn-tls-version": "TLSv1.2", + "x-amz-date": "20220803T092917Z", + "x-forwarded-proto": "https", + "x-forwarded-port": "443", + "x-forwarded-for": "123.123.123.123", + "accept": "application/xml", + "x-amzn-tls-cipher-suite": "ECDHE-RSA-AES128-GCM-SHA256", + "x-amzn-trace-id": "Root=1-63ea3fee-51ba94542feafa3928745ba3", + "host": "xxxxxxxxxxxxx.lambda-url.eu-central-1.on.aws", + "content-type": "application/json", + "accept-encoding": "gzip, deflate", + "user-agent": "Custom User Agent" + }, + "requestContext": { + "accountId": "123457890", + "apiId": "xxxxxxxxxxxxxxxxxxxx", + "authorizer": { + "iam": { + "accessKey": "AAAAAAAAAAAAAAAAAA", + "accountId": "123457890", + "callerId": "AAAAAAAAAAAAAAAAAA", + "cognitoIdentity": null, + "principalOrgId": "o-xxxxxxxxxxxx", + "userArn": "arn:aws:iam::AAAAAAAAAAAAAAAAAA:user/user", + "userId": "AAAAAAAAAAAAAAAAAA" + } + }, + "domainName": "xxxxxxxxxxxxx.lambda-url.eu-central-1.on.aws", + "domainPrefix": "xxxxxxxxxxxxx", + "http": { + "method": "GET", + "path": "/todos", + "protocol": "HTTP/1.1", + "sourceIp": "123.123.123.123", + "userAgent": "Custom User Agent" + }, + "requestId": "24f9ef37-8eb7-45fe-9dbc-a504169fd2f8", + "routeKey": "$default", + "stage": "$default", + "time": "03/Aug/2022:09:29:18 +0000", + "timeEpoch": 1659518958068 + }, + "isBase64Encoded": false +} diff --git a/examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.py b/examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.py new file mode 100644 index 00000000000..200a1988192 --- /dev/null +++ b/examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.py @@ -0,0 +1,28 @@ +import requests +from requests import Response + +from aws_lambda_powertools import Logger, Tracer +from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver +from aws_lambda_powertools.logging import correlation_paths +from aws_lambda_powertools.utilities.typing import LambdaContext + +tracer = Tracer() +logger = Logger() +app = LambdaFunctionUrlResolver() + + +@app.get("/todos") +@tracer.capture_method +def get_todos(): + todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos") + todos.raise_for_status() + + # for brevity, we'll limit to the first 10 only + return {"todos": todos.json()[:10]} + + +# You can continue to use other utilities just as before +@logger.inject_lambda_context(correlation_id_path=correlation_paths.LAMBDA_FUNCTION_URL) +@tracer.capture_lambda_handler +def lambda_handler(event: dict, context: LambdaContext) -> dict: + return app.resolve(event, context) From 1039c04ef117daa7fa26fdedc8c88670fdf61505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 3 Aug 2022 14:04:27 +0200 Subject: [PATCH 04/10] chore(docs): remove file commited by mistake --- .../core/event_handler/lambda_function_url.md | 544 ------------------ 1 file changed, 544 deletions(-) delete mode 100644 docs/core/event_handler/lambda_function_url.md diff --git a/docs/core/event_handler/lambda_function_url.md b/docs/core/event_handler/lambda_function_url.md deleted file mode 100644 index 64df750deeb..00000000000 --- a/docs/core/event_handler/lambda_function_url.md +++ /dev/null @@ -1,544 +0,0 @@ ---- -title: Lambda Function URL -description: Core utility ---- - -Event handler for AWS Lambda Function URLs. - -## Key Features - -* Lightweight routing to reduce boilerplate for Lambda Function URLs -* Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer -* Built-in integration with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"} for self-documented event schema - -## Getting started - -???+ tip - All examples shared in this documentation are available within the [project repository](https://github.com/awslabs/aws-lambda-powertools-python/tree/develop/examples){target="_blank"}. - -### Required resources - -You must have an existing AWS Lambda Function with a [configured Function URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html). - -This is the sample infrastructure for AWS Lambda we are using for the examples in this documentation. - -???+ info "There is no additional permissions or dependencies required to use this utility." - -```yaml title="AWS Serverless Application Model (SAM) example" ---8<-- "examples/event_handler_lambda_function_url/sam/template.yaml" -``` - -### Event Resolvers - -Before you decorate your functions to handle a given path and HTTP method(s), you need to initialize a resolver. - -A resolver will handle request resolution, including [one or more routers](#split-routes-with-router), and give you access to the current event via typed properties. - -For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, and `ALBResolver`. - -???+ info - We will use `APIGatewayRestResolver` as the default across examples. - -#### API Gateway REST API - -When using Amazon API Gateway REST API to front your Lambda functions, you can use `APIGatewayRestResolver`. - -Here's an example on how we can handle the `/todos` path. - -???+ info - We automatically serialize `Dict` responses as JSON, trim whitespace for compact responses, and set content-type to `application/json`. - -=== "getting_started_rest_api_resolver.py" - - ```python hl_lines="5 11 14 28" - --8<-- "examples/event_handler_rest/src/getting_started_rest_api_resolver.py" - ``` - -=== "getting_started_rest_api_resolver.json" - - This utility uses `path` and `httpMethod` to route to the right function. This helps make unit tests and local invocation easier too. - - ```json hl_lines="4-5" - --8<-- "examples/event_handler_rest/src/getting_started_rest_api_resolver.json" - ``` - -=== "getting_started_rest_api_resolver_output.json" - - ```json - --8<-- "examples/event_handler_rest/src/getting_started_rest_api_resolver_output.json" - ``` - -#### API Gateway HTTP API - -When using Amazon API Gateway HTTP API to front your Lambda functions, you can use `APIGatewayHttpResolver`. - -???+ note - Using HTTP API v1 payload? Use `APIGatewayRestResolver` instead. `APIGatewayHttpResolver` defaults to v2 payload. - -```python hl_lines="5 11" title="Using HTTP API resolver" ---8<-- "examples/event_handler_rest/src/getting_started_http_api_resolver.py" -``` - -#### Application Load Balancer - -When using Amazon Application Load Balancer (ALB) to front your Lambda functions, you can use `ALBResolver`. - -```python hl_lines="5 11" title="Using ALB resolver" ---8<-- "examples/event_handler_rest/src/getting_started_alb_api_resolver.py" -``` - -### Dynamic routes - -You can use `/todos/` to configure dynamic URL paths, where `` will be resolved at runtime. - -Each dynamic route you set must be part of your function signature. This allows us to call your function using keyword arguments when matching your dynamic route. - -???+ note - For brevity, we will only include the necessary keys for each sample request for the example to work. - -=== "dynamic_routes.py" - - ```python hl_lines="14 16" - --8<-- "examples/event_handler_rest/src/dynamic_routes.py" - ``` - -=== "dynamic_routes.json" - - ```json - --8<-- "examples/event_handler_rest/src/dynamic_routes.json" - ``` - -???+ tip - You can also nest dynamic paths, for example `/todos//`. - -???+ tip - You can also nest dynamic paths, for example `/todos//`. - -#### Catch-all routes - -???+ note - We recommend having explicit routes whenever possible; use catch-all routes sparingly. - -You can use a [regex](https://docs.python.org/3/library/re.html#regular-expression-syntax){target="_blank"} string to handle an arbitrary number of paths within a request, for example `.+`. - -You can also combine nested paths with greedy regex to catch in between routes. - -???+ warning - We choose the most explicit registered route that matches an incoming event. - -=== "dynamic_routes_catch_all.py" - - ```python hl_lines="11" - --8<-- "examples/event_handler_rest/src/dynamic_routes_catch_all.py" - ``` - -=== "dynamic_routes_catch_all.json" - - ```json - --8<-- "examples/event_handler_rest/src/dynamic_routes_catch_all.json" - ``` - -### HTTP Methods - -You can use named decorators to specify the HTTP method that should be handled in your functions. That is, `app.`, where the HTTP method could be `get`, `post`, `put`, `patch`, `delete`, and `options`. - -=== "http_methods.py" - - ```python hl_lines="14 17" - --8<-- "examples/event_handler_rest/src/http_methods.py" - ``` - -=== "http_methods.json" - - ```json - --8<-- "examples/event_handler_rest/src/http_methods.json" - ``` - -If you need to accept multiple HTTP methods in a single function, you can use the `route` method and pass a list of HTTP methods. - -```python hl_lines="15" title="Handling multiple HTTP Methods" ---8<-- "examples/event_handler_rest/src/http_methods_multiple.py" -``` - -???+ note - It is generally better to have separate functions for each HTTP method, as the functionality tends to differ depending on which method is used. - -### Accessing request details - -Event Handler integrates with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"}, and it exposes their respective resolver request details and convenient methods under `app.current_event`. - -That is why you see `app.resolve(event, context)` in every example. This allows Event Handler to resolve requests, and expose data like `app.lambda_context` and `app.current_event`. - -#### Query strings and payload - -Within `app.current_event` property, you can access all available query strings as a dictionary via `query_string_parameters`, or a specific one via `get_query_string_value` method. - -You can access the raw payload via `body` property, or if it's a JSON string you can quickly deserialize it via `json_body` property - like the earlier example in the [HTTP Methods](#http-methods) section. - -```python hl_lines="19 24" title="Accessing query strings and raw payload" ---8<-- "examples/event_handler_rest/src/accessing_request_details.py" -``` - -#### Headers - -Similarly to [Query strings](#query-strings-and-payload), you can access headers as dictionary via `app.current_event.headers`, or by name via `get_header_value`. - -```python hl_lines="19" title="Accessing HTTP Headers" ---8<-- "examples/event_handler_rest/src/accessing_request_details_headers.py" -``` - -### Handling not found routes - -By default, we return `404` for any unmatched route. - -You can use **`not_found`** decorator to override this behavior, and return a custom **`Response`**. - -```python hl_lines="14 18" title="Handling not found" ---8<-- "examples/event_handler_rest/src/not_found_routes.py" -``` - -### Exception handling - -You can use **`exception_handler`** decorator with any Python exception. This allows you to handle a common exception outside your route, for example validation errors. - -```python hl_lines="13-14" title="Exception handling" ---8<-- "examples/event_handler_rest/src/exception_handling.py" -``` - -### Raising HTTP errors - -You can easily raise any HTTP Error back to the client using `ServiceError` exception. This ensures your Lambda function doesn't fail but return the correct HTTP response signalling the error. - -???+ info - If you need to send custom headers, use [Response](#fine-grained-responses) class instead. - -We provide pre-defined errors for the most popular ones such as HTTP 400, 401, 404, 500. - -```python hl_lines="6-11 23 28 33 38 43" title="Raising common HTTP Status errors (4xx, 5xx)" ---8<-- "examples/event_handler_rest/src/raising_http_errors.py" -``` - -### Custom Domain API Mappings - -When using [Custom Domain API Mappings feature](https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-mappings.html){target="_blank"}, you must use **`strip_prefixes`** param in the `APIGatewayRestResolver` constructor. - -**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/awslabs/aws-lambda-powertools-roadmap/issues/34). - -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. - -=== "custom_api_mapping.py" - - ```python hl_lines="8" - --8<-- "examples/event_handler_rest/src/custom_api_mapping.py" - ``` - -=== "custom_api_mapping.json" - - ```json - --8<-- "examples/event_handler_rest/src/custom_api_mapping.json" - ``` - -???+ note - After removing a path prefix with `strip_prefixes`, the new root path will automatically be mapped to the path argument of `/`. - - 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 `/`. - -## Advanced - -### CORS - -You can configure CORS at the `APIGatewayRestResolver` constructor via `cors` parameter using the `CORSConfig` class. - -This will ensure that CORS headers are always returned as part of the response when your functions match the path invoked. - -???+ tip - Optionally disable CORS on a per path basis with `cors=False` parameter. - -=== "setting_cors.py" - - ```python hl_lines="5 11-12 34" - --8<-- "examples/event_handler_rest/src/setting_cors.py" - ``` - -=== "setting_cors_output.json" - - ```json - --8<-- "examples/event_handler_rest/src/setting_cors_output.json" - ``` - -#### Pre-flight - -Pre-flight (OPTIONS) calls are typically handled at the API Gateway level as per [our sample infrastructure](#required-resources), no Lambda integration necessary. However, ALB expects you to handle pre-flight requests. - -For convenience, we automatically handle that for you as long as you [setup CORS in the constructor level](#cors). - -#### Defaults - -For convenience, these are the default values when using `CORSConfig` to enable CORS: - -???+ warning - Always configure `allow_origin` when using in production. - -| Key | Value | Note | -| -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **[allow_origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin){target="_blank"}**: `str` | `*` | Only use the default value for development. **Never use `*` for production** unless your use case requires it | -| **[allow_headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers){target="_blank"}**: `List[str]` | `[Authorization, Content-Type, X-Amz-Date, X-Api-Key, X-Amz-Security-Token]` | Additional headers will be appended to the default list for your convenience | -| **[expose_headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers){target="_blank"}**: `List[str]` | `[]` | Any additional header beyond the [safe listed by CORS specification](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header){target="_blank"}. | -| **[max_age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age){target="_blank"}**: `int` | `` | Only for pre-flight requests if you choose to have your function to handle it instead of API Gateway | -| **[allow_credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials){target="_blank"}**: `bool` | `False` | Only necessary when you need to expose cookies, authorization headers or TLS client certificates. | - -### Fine grained responses - -You can use the `Response` class to have full control over the response, for example you might want to add additional headers or set a custom Content-type. - -=== "fine_grained_responses.py" - - ```python hl_lines="7 24-28" - --8<-- "examples/event_handler_rest/src/fine_grained_responses.py" - ``` - -=== "fine_grained_responses_output.json" - - ```json - --8<-- "examples/event_handler_rest/src/fine_grained_responses_output.json" - ``` - -### Compress - -You can compress with gzip and base64 encode your responses via `compress` parameter. - -???+ warning - The client must send the `Accept-Encoding` header, otherwise a normal response will be sent. - -=== "compressing_responses.py" - - ```python hl_lines="14" - --8<-- "examples/event_handler_rest/src/compressing_responses.py" - ``` - -=== "compressing_responses.json" - - ```json - --8<-- "examples/event_handler_rest/src/compressing_responses.json" - ``` - -=== "compressing_responses_output.json" - - ```json - --8<-- "examples/event_handler_rest/src/compressing_responses_output.json" - ``` - -### Binary responses - -For convenience, we automatically base64 encode binary responses. You can also use in combination with `compress` parameter if your client supports gzip. - -Like `compress` feature, the client must send the `Accept` header with the correct media type. - -???+ warning - This feature requires API Gateway to configure binary media types, see [our sample infrastructure](#required-resources) for reference. - -=== "binary_responses.py" - - ```python hl_lines="14 20" - --8<-- "examples/event_handler_rest/src/binary_responses.py" - ``` - -=== "binary_responses_logo.svg" - - ```xml - --8<-- "examples/event_handler_rest/src/binary_responses_logo.svg" - ``` - -=== "binary_responses.json" - - ```json - --8<-- "examples/event_handler_rest/src/binary_responses.json" - ``` - -=== "binary_responses_output.json" - - ```json - --8<-- "examples/event_handler_rest/src/binary_responses_output.json" - ``` - -### Debug mode - -You can enable debug mode via `debug` param, or via `POWERTOOLS_EVENT_HANDLER_DEBUG` [environment variable](../../index.md#environment-variables). - -This will enable full tracebacks errors in the response, print request and responses, and set CORS in development mode. - -???+ danger - This might reveal sensitive information in your logs and relax CORS restrictions, use it sparingly. - - It's best to use for local development only! - -```python hl_lines="11" title="Enabling debug mode" ---8<-- "examples/event_handler_rest/src/debug_mode.py" -``` - -### Custom serializer - -You can instruct API Gateway handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. - -```python hl_lines="35 40" title="Using a custom JSON serializer for responses" ---8<-- "examples/event_handler_rest/src/custom_serializer.py" -``` - -### Split routes with Router - -As you grow the number of routes a given Lambda function should handle, it is natural to split routes into separate files to ease maintenance - That's where the `Router` feature is useful. - -Let's assume you have `split_route.py` as your Lambda function entrypoint and routes in `split_route_module.py`. This is how you'd use the `Router` feature. - -=== "split_route_module.py" - - We import **Router** instead of **APIGatewayRestResolver**; syntax wise is exactly the same. - - ```python hl_lines="5 13 16 25 28" - --8<-- "examples/event_handler_rest/src/split_route_module.py" - ``` - -=== "split_route.py" - - We use `include_router` method and include all user routers registered in the `router` global object. - - ```python hl_lines="11" - --8<-- "examples/event_handler_rest/src/split_route.py" - ``` - -#### Route prefix - -In the previous example, `split_route_module.py` routes had a `/todos` prefix. This might grow over time and become repetitive. - -When necessary, you can set a prefix when including a router object. This means you could remove `/todos` prefix altogether. - -=== "split_route_prefix.py" - - ```python hl_lines="12" - --8<-- "examples/event_handler_rest/src/split_route_prefix.py" - ``` - -=== "split_route_prefix_module.py" - - ```python hl_lines="13 25" - --8<-- "examples/event_handler_rest/src/split_route_prefix_module.py" - ``` - -#### Sample layout - -This is a sample project layout for a monolithic function with routes split in different files (`/todos`, `/health`). - -```shell hl_lines="4 7 10 12-13" title="Sample project layout" -. -├── pyproject.toml # project app & dev dependencies; poetry, pipenv, etc. -├── poetry.lock -├── src -│ ├── __init__.py -│ ├── requirements.txt # sam build detect it automatically due to CodeUri: src. poetry export --format src/requirements.txt -│ └── todos -│ ├── __init__.py -│ ├── main.py # this will be our todos Lambda fn; it could be split in folders if we want separate fns same code base -│ └── routers # routers module -│ ├── __init__.py -│ ├── health.py # /health routes. from routers import todos; health.router -│ └── todos.py # /todos routes. from .routers import todos; todos.router -├── template.yml # SAM. CodeUri: src, Handler: todos.main.lambda_handler -└── tests - ├── __init__.py - ├── unit - │ ├── __init__.py - │ └── test_todos.py # unit tests for the todos router - │ └── test_health.py # unit tests for the health router - └── functional - ├── __init__.py - ├── conftest.py # pytest fixtures for the functional tests - └── test_main.py # functional tests for the main lambda handler -``` - -### Considerations - -This utility is optimized for fast startup, minimal feature set, and to quickly on-board customers familiar with frameworks like Flask — it's not meant to be a fully fledged framework. - -Event Handler naturally leads to a single Lambda function handling multiple routes for a given service, which can be eventually broken into multiple functions. - -Both single (monolithic) and multiple functions (micro) offer different set of trade-offs worth knowing. - -???+ tip - TL;DR. Start with a monolithic function, add additional functions with new handlers, and possibly break into micro functions if necessary. - -#### Monolithic function - -![Monolithic function sample](./../../media/monolithic-function.png) - -A monolithic function means that your final code artifact will be deployed to a single function. This is generally the best approach to start. - -_**Benefits**_ - -* **Code reuse**. It's easier to reason about your service, modularize it and reuse code as it grows. Eventually, it can be turned into a standalone library. -* **No custom tooling**. Monolithic functions are treated just like normal Python packages; no upfront investment in tooling. -* **Faster deployment and debugging**. Whether you use all-at-once, linear, or canary deployments, a monolithic function is a single deployable unit. IDEs like PyCharm and VSCode have tooling to quickly profile, visualize, and step through debug any Python package. - -_**Downsides**_ - -* **Cold starts**. Frequent deployments and/or high load can diminish the benefit of monolithic functions depending on your latency requirements, due to [Lambda scaling model](https://docs.aws.amazon.com/lambda/latest/dg/invocation-scaling.html){target="_blank"}. Always load test to pragmatically balance between your customer experience and development cognitive load. -* **Granular security permissions**. The micro function approach enables you to use fine-grained permissions & access controls, separate external dependencies & code signing at the function level. Conversely, you could have multiple functions while duplicating the final code artifact in a monolithic approach. - * Regardless, least privilege can be applied to either approaches. -* **Higher risk per deployment**. A misconfiguration or invalid import can cause disruption if not caught earlier in automated testing. Multiple functions can mitigate misconfigurations but they would still share the same code artifact. You can further minimize risks with multiple environments in your CI/CD pipeline. - -#### Micro function - -![Micro function sample](./../../media/micro-function.png) - -A micro function means that your final code artifact will be different to each function deployed. This is generally the approach to start if you're looking for fine-grain control and/or high load on certain parts of your service. - -**Benefits** - -* **Granular scaling**. A micro function can benefit from the [Lambda scaling model](https://docs.aws.amazon.com/lambda/latest/dg/invocation-scaling.html){target="_blank"} to scale differently depending on each part of your application. Concurrency controls and provisioned concurrency can also be used at a granular level for capacity management. -* **Discoverability**. Micro functions are easier do visualize when using distributed tracing. Their high-level architectures can be self-explanatory, and complexity is highly visible — assuming each function is named to the business purpose it serves. -* **Package size**. An independent function can be significant smaller (KB vs MB) depending on external dependencies it require to perform its purpose. Conversely, a monolithic approach can benefit from [Lambda Layers](https://docs.aws.amazon.com/lambda/latest/dg/invocation-layers.html){target="_blank"} to optimize builds for external dependencies. - -**Downsides** - -* **Upfront investment**. You need custom build tooling to bundle assets, including [C bindings for runtime compatibility](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html){target="_blank"}. `Operations become more elaborate — you need to standardize tracing labels/annotations, structured logging, and metrics to pinpoint root causes. - * Engineering discipline is necessary for both approaches. Micro-function approach however requires further attention in consistency as the number of functions grow, just like any distributed system. -* **Harder to share code**. Shared code must be carefully evaluated to avoid unnecessary deployments when that changes. Equally, if shared code isn't a library, -your development, building, deployment tooling need to accommodate the distinct layout. -* **Slower safe deployments**. Safely deploying multiple functions require coordination — AWS CodeDeploy deploys and verifies each function sequentially. This increases lead time substantially (minutes to hours) depending on the deployment strategy you choose. You can mitigate it by selectively enabling it in prod-like environments only, and where the risk profile is applicable. - * Automated testing, operational and security reviews are essential to stability in either approaches. - -## Testing your code - -You can test your routes by passing a proxy event request where `path` and `httpMethod`. - -=== "assert_http_response.py" - - ```python hl_lines="21-24" - --8<-- "examples/event_handler_rest/src/assert_http_response.py" - ``` - -=== "assert_http_response_module.py" - - ```python - --8<-- "examples/event_handler_rest/src/assert_http_response_module.py" - ``` - -## FAQ - -**What's the difference between this utility and frameworks like Chalice?** - -Chalice is a full featured microframework that manages application and infrastructure. This utility, however, is largely focused on routing to reduce boilerplate and expects you to setup and manage infrastructure with your framework of choice. - -That said, [Chalice has native integration with Lambda Powertools](https://aws.github.io/chalice/topics/middleware.html){target="_blank"} if you're looking for a more opinionated and web framework feature set. - -**What happened to `ApiGatewayResolver`?** - -It's been superseded by more explicit resolvers like `APIGatewayRestResolver`, `APIGatewayHttpResolver`, and `ALBResolver`. - -`ApiGatewayResolver` handled multiple types of event resolvers for convenience via `proxy_type` param. However, -it made it impossible for static checkers like Mypy and IDEs IntelliSense to know what properties a `current_event` would have due to late bound resolution. - -This provided a suboptimal experience for customers not being able to find all properties available besides common ones between API Gateway REST, HTTP, and ALB - while manually annotating `app.current_event` would work it is not the experience we want to provide to customers. - -`ApiGatewayResolver` will be deprecated in v2 and have appropriate warnings as soon as we have a v2 draft. From febe483326f0dc269f114a7fa770a0290c100eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 3 Aug 2022 14:12:44 +0200 Subject: [PATCH 05/10] chore(docs): updated links --- .../event_handler/lambda_function_url.py | 35 +++++++++++++++++++ .../data_classes/lambda_function_url_event.py | 9 +++-- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/event_handler/lambda_function_url.py b/aws_lambda_powertools/event_handler/lambda_function_url.py index 592a662f3be..5c2482dcb12 100644 --- a/aws_lambda_powertools/event_handler/lambda_function_url.py +++ b/aws_lambda_powertools/event_handler/lambda_function_url.py @@ -6,6 +6,41 @@ class LambdaFunctionUrlResolver(ApiGatewayResolver): + """AWS Lambda Function URL resolver + + Notes: + ----- + For now, this seems to work the same way as API Gateway HTTP APIs Payload Format Version 2.0. + + Documentation: + - https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html + - https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + + Examples + -------- + Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import LambdaFunctoinUrlResolver + + tracer = Tracer() + app = LambdaFunctoinUrlResolver() + + @app.get("/get-call") + def simple_get(): + return {"message": "Foo"} + + @app.post("/post-call") + def simple_post(): + post_data: dict = app.current_event.json_body + return {"message": post_data["value"]} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + """ + current_event: LambdaFunctionUrlEvent def __init__( diff --git a/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py b/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py index e86fd55a1fe..baaa47191c2 100644 --- a/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py +++ b/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py @@ -6,14 +6,13 @@ class LambdaFunctionUrlEvent(APIGatewayProxyEventV2): Notes: ----- - For now, this seems to follow the exact payload as HTTP APIs Payload Format Version 2.0. - Certain keys in this payload format don't make sense for function urls (e.g: `routeKey`). - These keys will always be null. + For now, this seems to follow the exact same payload as HTTP APIs Payload Format Version 2.0. + Certain keys in this payload format don't make sense for function urls (e.g: `routeKey`, `stage`). + These keys will have default values that come on the payload, but they are not useful since they can't be changed. Documentation: + - https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html - https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads - - https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html - """ pass From 184d84f5807c48056dc1c4f878a448d5e008bb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 3 Aug 2022 14:21:20 +0200 Subject: [PATCH 06/10] chore(docs): typo --- docs/utilities/data_classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 2d1d32b4366..a450c8788e4 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -845,7 +845,7 @@ or plain text, depending on the original payload. === "app.py" ```python - from aws_lambda_powertools.utilities.data_classes import event_source, LambdaFunctoinUrlEvent + from aws_lambda_powertools.utilities.data_classes import event_source, LambdaFunctionUrlEvent @event_source(data_class=LambdaFunctionUrlEvent) def lambda_handler(event: LambdaFunctionUrlEvent, context): From 0711e6d2f4cffe01220b9a2d223d92b6d1d4a637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Wed, 3 Aug 2022 22:29:10 +0200 Subject: [PATCH 07/10] chore: typos --- aws_lambda_powertools/event_handler/lambda_function_url.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/event_handler/lambda_function_url.py b/aws_lambda_powertools/event_handler/lambda_function_url.py index 5c2482dcb12..79b801d3cc4 100644 --- a/aws_lambda_powertools/event_handler/lambda_function_url.py +++ b/aws_lambda_powertools/event_handler/lambda_function_url.py @@ -22,10 +22,10 @@ class LambdaFunctionUrlResolver(ApiGatewayResolver): ```python from aws_lambda_powertools import Tracer - from aws_lambda_powertools.event_handler import LambdaFunctoinUrlResolver + from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver tracer = Tracer() - app = LambdaFunctoinUrlResolver() + app = LambdaFunctionUrlResolver() @app.get("/get-call") def simple_get(): From a47f6eb686500b2427070a339b0460ef37db970b Mon Sep 17 00:00:00 2001 From: Ruben Fonseca Date: Thu, 4 Aug 2022 12:18:51 +0200 Subject: [PATCH 08/10] chore(event_handlers): apply suggestions from code review Co-authored-by: Heitor Lessa --- .../event_handler/lambda_function_url.py | 6 +++--- aws_lambda_powertools/logging/correlation_paths.py | 2 +- .../utilities/data_classes/api_gateway_proxy_event.py | 10 +++++----- aws_lambda_powertools/utilities/data_classes/common.py | 1 + .../data_classes/lambda_function_url_event.py | 6 +++--- docs/core/event_handler/api_gateway.md | 9 +++++---- docs/index.md | 2 +- .../sam/template.yaml | 2 +- 8 files changed, 20 insertions(+), 18 deletions(-) diff --git a/aws_lambda_powertools/event_handler/lambda_function_url.py b/aws_lambda_powertools/event_handler/lambda_function_url.py index 79b801d3cc4..6d5924e79b9 100644 --- a/aws_lambda_powertools/event_handler/lambda_function_url.py +++ b/aws_lambda_powertools/event_handler/lambda_function_url.py @@ -10,7 +10,7 @@ class LambdaFunctionUrlResolver(ApiGatewayResolver): Notes: ----- - For now, this seems to work the same way as API Gateway HTTP APIs Payload Format Version 2.0. + Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0. Documentation: - https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html @@ -18,7 +18,7 @@ class LambdaFunctionUrlResolver(ApiGatewayResolver): Examples -------- - Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator + Simple example integrating with Tracer ```python from aws_lambda_powertools import Tracer @@ -34,7 +34,7 @@ def simple_get(): @app.post("/post-call") def simple_post(): post_data: dict = app.current_event.json_body - return {"message": post_data["value"]} + return {"message": post_data} @tracer.capture_lambda_handler def lambda_handler(event, context): diff --git a/aws_lambda_powertools/logging/correlation_paths.py b/aws_lambda_powertools/logging/correlation_paths.py index 01de17eddcb..823dc47acbe 100644 --- a/aws_lambda_powertools/logging/correlation_paths.py +++ b/aws_lambda_powertools/logging/correlation_paths.py @@ -6,5 +6,5 @@ APPSYNC_RESOLVER = 'request.headers."x-amzn-trace-id"' APPLICATION_LOAD_BALANCER = 'headers."x-amzn-trace-id"' EVENT_BRIDGE = "id" -LAMBDA_FUNCTION_URL = "requestContext.requestId" +LAMBDA_FUNCTION_URL = API_GATEWAY_REST S3_OBJECT_LAMBDA = "xAmzRequestId" diff --git a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py index 1fc66c42c3b..57c62f46b1a 100644 --- a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py +++ b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py @@ -127,21 +127,21 @@ def caller_id(self) -> Optional[str]: def cognito_amr(self) -> Optional[List[str]]: """This represents how the user was authenticated. AMR stands for Authentication Methods References as per the openid spec""" - cognito_identity = self["cognitoIdentity"] or {} + cognito_identity = self["cognitoIdentity"] or {} # not available in FunctionURL return cognito_identity.get("amr") @property def cognito_identity_id(self) -> Optional[str]: """The Amazon Cognito identity ID of the caller making the request. Available only if the request was signed with Amazon Cognito credentials.""" - cognito_identity = self.get("cognitoIdentity") or {} + cognito_identity = self.get("cognitoIdentity") or {} # not available in FunctionURL return cognito_identity.get("identityId") @property def cognito_identity_pool_id(self) -> Optional[str]: """The Amazon Cognito identity pool ID of the caller making the request. Available only if the request was signed with Amazon Cognito credentials.""" - cognito_identity = self.get("cognitoIdentity") or {} + cognito_identity = self.get("cognitoIdentity") or {} # not available in FunctionURL return cognito_identity.get("identityPoolId") @property @@ -163,12 +163,12 @@ def user_id(self) -> Optional[str]: class RequestContextV2Authorizer(DictWrapper): @property def jwt_claim(self) -> Optional[Dict[str, Any]]: - jwt = self.get("jwt") or {} + jwt = self.get("jwt") or {} # not available in FunctionURL return jwt.get("claims") @property def jwt_scopes(self) -> Optional[List[str]]: - jwt = self.get("jwt") or {} + jwt = self.get("jwt") or {} # not available in FunctionURL return jwt.get("scopes") @property diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index 47d28491c46..2109ee3dd3e 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -395,6 +395,7 @@ def time_epoch(self) -> int: @property def authentication(self) -> Optional[RequestContextClientCert]: """Optional when using mutual TLS authentication""" + # FunctionURL might have NONE as AuthZ authentication = self["requestContext"].get("authentication") or {} client_cert = authentication.get("clientCert") return None if client_cert is None else RequestContextClientCert(client_cert) diff --git a/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py b/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py index baaa47191c2..2b88918f17b 100644 --- a/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py +++ b/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py @@ -6,9 +6,9 @@ class LambdaFunctionUrlEvent(APIGatewayProxyEventV2): Notes: ----- - For now, this seems to follow the exact same payload as HTTP APIs Payload Format Version 2.0. - Certain keys in this payload format don't make sense for function urls (e.g: `routeKey`, `stage`). - These keys will have default values that come on the payload, but they are not useful since they can't be changed. + Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0. + + Keys related to API Gateway features not available in Function URL use a sentinel value (e.g.`routeKey`, `stage`). Documentation: - https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 1f61039f195..1358f545eb8 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -97,7 +97,7 @@ When using Amazon Application Load Balancer (ALB) to front your Lambda functions #### Lambda Function URL -When using an [AWS Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), you can use `LambdaFunctionUrlResolver`. +When using [AWS Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), you can use `LambdaFunctionUrlResolver`. === "getting_started_lambda_function_url_resolver.py" @@ -294,7 +294,7 @@ This will ensure that CORS headers are always returned as part of the response w #### Pre-flight -Pre-flight (OPTIONS) calls are typically handled at the API Gateway or Lambda Function URL level as per [our sample infrastructure](#required-resources), no Lambda integration necessary. However, ALB expects you to handle pre-flight requests. +Pre-flight (OPTIONS) calls are typically handled at the API Gateway or Lambda Function URL level as per [our sample infrastructure](#required-resources), no Lambda integration is necessary. However, ALB expects you to handle pre-flight requests. For convenience, we automatically handle that for you as long as you [setup CORS in the constructor level](#cors). @@ -362,8 +362,9 @@ Like `compress` feature, the client must send the `Accept` header with the corre ???+ warning This feature requires API Gateway to configure binary media types, see [our sample infrastructure](#required-resources) for reference. - For Lambda Function URLs, no additional configuration is necessary. +???+ note + Lambda Function URLs handle binary media types automatically. === "binary_responses.py" ```python hl_lines="14 20" @@ -405,7 +406,7 @@ This will enable full tracebacks errors in the response, print request and respo ### Custom serializer -You can instruct an event handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. +You can instruct event handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing. ```python hl_lines="35 40" title="Using a custom JSON serializer for responses" --8<-- "examples/event_handler_rest/src/custom_serializer.py" diff --git a/docs/index.md b/docs/index.md index a270db26f42..58cecbebc9c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -417,7 +417,7 @@ Core utilities such as Tracing, Logging, Metrics, and Event Handler will be avai | [Logger](./core/logger.md) | Structured logging made easier, and decorator to enrich structured logging with key Lambda context details | | [Metrics](./core/metrics.md) | Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF) | | [Event handler: AppSync](./core/event_handler/appsync.md) | AppSync event handler for Lambda Direct Resolver and Amplify GraphQL Transformer function | -| [Event handler: API Gateway, ALB and Lambda Function URL](https://awslabs.github.io/aws-lambda-powertools-python/latest/core/event_handler/api_gateway/) | Amazon API Gateway REST/HTTP API and ALB event handler for Lambda functions invoked using Proxy integration | +| [Event handler: API Gateway, ALB and Lambda Function URL](https://awslabs.github.io/aws-lambda-powertools-python/latest/core/event_handler/api_gateway/) | Amazon API Gateway REST/HTTP API and ALB event handler for Lambda functions invoked using Proxy integration, and Lambda Function URL | | [Middleware factory](./utilities/middleware_factory.md) | Decorator factory to create your own middleware to run logic before, and after each Lambda invocation | | [Parameters](./utilities/parameters.md) | Retrieve parameter values from AWS Systems Manager Parameter Store, AWS Secrets Manager, or Amazon DynamoDB, and cache them for a specific amount of time | | [Batch processing](./utilities/batch.md) | Handle partial failures for AWS SQS batch processing | diff --git a/examples/event_handler_lambda_function_url/sam/template.yaml b/examples/event_handler_lambda_function_url/sam/template.yaml index 34b7f053263..d103a6bbfb3 100644 --- a/examples/event_handler_lambda_function_url/sam/template.yaml +++ b/examples/event_handler_lambda_function_url/sam/template.yaml @@ -28,4 +28,4 @@ Resources: CodeUri: ../src Description: API handler function FunctionUrlConfig: - AuthType: NONE + AuthType: NONE # AWS_IAM for added security beyond sample documentation From 8ccf4b674a6982f1dd33d125afb447c83aa0c59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 4 Aug 2022 12:22:38 +0200 Subject: [PATCH 09/10] chore(tests): add missing new line --- tests/events/lambdaFunctionUrlEvent.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/events/lambdaFunctionUrlEvent.json b/tests/events/lambdaFunctionUrlEvent.json index 94e1fbba8cd..dd0cf8ea345 100644 --- a/tests/events/lambdaFunctionUrlEvent.json +++ b/tests/events/lambdaFunctionUrlEvent.json @@ -48,5 +48,5 @@ "body": "Hello from client!", "pathParameters": null, "isBase64Encoded": false, - "stageVariables": null -} \ No newline at end of file + "stageVariables": null, +} From eaebeeeafac7f5bf9f35ca76912d8aeb0541b51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=BAben=20Fonseca?= Date: Thu, 4 Aug 2022 12:36:16 +0200 Subject: [PATCH 10/10] chore(tests): typo --- tests/events/lambdaFunctionUrlEvent.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/events/lambdaFunctionUrlEvent.json b/tests/events/lambdaFunctionUrlEvent.json index dd0cf8ea345..bf52342b66d 100644 --- a/tests/events/lambdaFunctionUrlEvent.json +++ b/tests/events/lambdaFunctionUrlEvent.json @@ -48,5 +48,5 @@ "body": "Hello from client!", "pathParameters": null, "isBase64Encoded": false, - "stageVariables": null, + "stageVariables": null }