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..6d5924e79b9 --- /dev/null +++ b/aws_lambda_powertools/event_handler/lambda_function_url.py @@ -0,0 +1,53 @@ +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): + """AWS Lambda Function URL resolver + + Notes: + ----- + 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 + - https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + + Examples + -------- + Simple example integrating with Tracer + + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver + + tracer = Tracer() + app = LambdaFunctionUrlResolver() + + @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} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + """ + + 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..823dc47acbe 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 = API_GATEWAY_REST S3_OBJECT_LAMBDA = "xAmzRequestId" 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..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,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 {} # 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.""" - return self["cognitoIdentity"].get("identityId") + 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.""" - return self["cognitoIdentity"].get("identityPoolId") + cognito_identity = self.get("cognitoIdentity") or {} # not available in FunctionURL + 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 {} # not available in FunctionURL + 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 {} # not available in FunctionURL + 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..2109ee3dd3e 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -395,5 +395,7 @@ 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") + # 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 new file mode 100644 index 00000000000..2b88918f17b --- /dev/null +++ b/aws_lambda_powertools/utilities/data_classes/lambda_function_url_event.py @@ -0,0 +1,18 @@ +from aws_lambda_powertools.utilities.data_classes.api_gateway_proxy_event import APIGatewayProxyEventV2 + + +class LambdaFunctionUrlEvent(APIGatewayProxyEventV2): + """AWS Lambda Function URL event + + Notes: + ----- + 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 + - https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads + """ + + pass diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 707d9687e63..1358f545eb8 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 [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 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). @@ -339,6 +363,8 @@ 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. +???+ note + Lambda Function URLs handle binary media types automatically. === "binary_responses.py" ```python hl_lines="14 20" @@ -380,7 +406,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 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 +527,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/index.md b/docs/index.md index b9ae8349c9f..58cecbebc9c 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, 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 | +| [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/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 8353d904bb1..a450c8788e4 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, LambdaFunctionUrlEvent + + @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/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..d103a6bbfb3 --- /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 # AWS_IAM for added security beyond sample documentation 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) diff --git a/tests/events/lambdaFunctionUrlEvent.json b/tests/events/lambdaFunctionUrlEvent.json new file mode 100644 index 00000000000..bf52342b66d --- /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 +} 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" 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