diff --git a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py
index 1b118d28117..7d42fd81ad6 100644
--- a/aws_lambda_powertools/utilities/parser/envelopes/__init__.py
+++ b/aws_lambda_powertools/utilities/parser/envelopes/__init__.py
@@ -5,6 +5,7 @@
from .dynamodb import DynamoDBStreamEnvelope
from .event_bridge import EventBridgeEnvelope
from .kinesis import KinesisDataStreamEnvelope
+from .lambda_function_url import LambdaFunctionUrlEnvelope
from .sns import SnsEnvelope, SnsSqsEnvelope
from .sqs import SqsEnvelope
@@ -15,6 +16,7 @@
"DynamoDBStreamEnvelope",
"EventBridgeEnvelope",
"KinesisDataStreamEnvelope",
+ "LambdaFunctionUrlEnvelope",
"SnsEnvelope",
"SnsSqsEnvelope",
"SqsEnvelope",
diff --git a/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py b/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py
new file mode 100644
index 00000000000..e54fb081b65
--- /dev/null
+++ b/aws_lambda_powertools/utilities/parser/envelopes/lambda_function_url.py
@@ -0,0 +1,32 @@
+import logging
+from typing import Any, Dict, Optional, Type, Union
+
+from ..models import LambdaFunctionUrlModel
+from ..types import Model
+from .base import BaseEnvelope
+
+logger = logging.getLogger(__name__)
+
+
+class LambdaFunctionUrlEnvelope(BaseEnvelope):
+ """Lambda function URL envelope to extract data within body key"""
+
+ def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Optional[Model]:
+ """Parses data found with model provided
+
+ Parameters
+ ----------
+ data : Dict
+ Lambda event to be parsed
+ model : Type[Model]
+ Data model provided to parse after extracting data using envelope
+
+ Returns
+ -------
+ Any
+ Parsed detail payload with model provided
+ """
+ logger.debug(f"Parsing incoming data with Lambda function URL model {LambdaFunctionUrlModel}")
+ parsed_envelope: LambdaFunctionUrlModel = LambdaFunctionUrlModel.parse_obj(data)
+ logger.debug(f"Parsing event payload in `detail` with {model}")
+ return self._parse(data=parsed_envelope.body, model=model)
diff --git a/aws_lambda_powertools/utilities/parser/models/__init__.py b/aws_lambda_powertools/utilities/parser/models/__init__.py
index e3fb50a2d5d..11ab6501fa9 100644
--- a/aws_lambda_powertools/utilities/parser/models/__init__.py
+++ b/aws_lambda_powertools/utilities/parser/models/__init__.py
@@ -18,6 +18,7 @@
from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel
from .event_bridge import EventBridgeModel
from .kinesis import KinesisDataStreamModel, KinesisDataStreamRecord, KinesisDataStreamRecordPayload
+from .lambda_function_url import LambdaFunctionUrlModel
from .s3 import S3Model, S3RecordModel
from .s3_object_event import (
S3ObjectConfiguration,
@@ -66,6 +67,7 @@
"KinesisDataStreamModel",
"KinesisDataStreamRecord",
"KinesisDataStreamRecordPayload",
+ "LambdaFunctionUrlModel",
"S3Model",
"S3RecordModel",
"S3ObjectLambdaEvent",
diff --git a/aws_lambda_powertools/utilities/parser/models/apigwv2.py b/aws_lambda_powertools/utilities/parser/models/apigwv2.py
index f97dad3bcb0..cb1f830bb47 100644
--- a/aws_lambda_powertools/utilities/parser/models/apigwv2.py
+++ b/aws_lambda_powertools/utilities/parser/models/apigwv2.py
@@ -20,7 +20,7 @@ class RequestContextV2AuthorizerIam(BaseModel):
principalOrgId: Optional[str]
userArn: Optional[str]
userId: Optional[str]
- cognitoIdentity: RequestContextV2AuthorizerIamCognito
+ cognitoIdentity: Optional[RequestContextV2AuthorizerIamCognito]
class RequestContextV2AuthorizerJwt(BaseModel):
diff --git a/aws_lambda_powertools/utilities/parser/models/lambda_function_url.py b/aws_lambda_powertools/utilities/parser/models/lambda_function_url.py
new file mode 100644
index 00000000000..2088ab9fa04
--- /dev/null
+++ b/aws_lambda_powertools/utilities/parser/models/lambda_function_url.py
@@ -0,0 +1,18 @@
+from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventV2Model
+
+
+class LambdaFunctionUrlModel(APIGatewayProxyEventV2Model):
+ """AWS Lambda Function URL model
+
+ 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/utilities/parser.md b/docs/utilities/parser.md
index cb69cf9699b..97b005a9fb5 100644
--- a/docs/utilities/parser.md
+++ b/docs/utilities/parser.md
@@ -2,6 +2,7 @@
title: Parser
description: Utility
---
+
This utility provides data parsing and deep validation using [Pydantic](https://pydantic-docs.helpmanual.io/).
@@ -166,6 +167,7 @@ Parser comes with the following built-in models:
| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service |
| **APIGatewayProxyEventModel** | Lambda Event Source payload for Amazon API Gateway |
| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload |
+| **LambdaFunctionUrlModel** | Lambda Event Source payload for Lambda Function URL payload |
### extending built-in models
@@ -305,6 +307,7 @@ Parser comes with the following built-in envelopes, where `Model` in the return
| **SnsSqsEnvelope** | 1. Parses data using `SqsModel`.
2. Parses SNS records in `body` key using `SnsNotificationModel`.
3. Parses data in `Message` key using your model and return them in a list. | `List[Model]` |
| **ApiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventModel`.
2. Parses `body` key using your model and returns it. | `Model` |
| **ApiGatewayV2Envelope** | 1. Parses data using `APIGatewayProxyEventV2Model`.
2. Parses `body` key using your model and returns it. | `Model` |
+| **LambdaFunctionUrlEnvelope** | 1. Parses data using `LambdaFunctionUrlModel`.
2. Parses `body` key using your model and returns it. | `Model` |
### Bringing your own envelope
diff --git a/tests/functional/parser/schemas.py b/tests/functional/parser/schemas.py
index 8ff56f703a7..79a74f8eb53 100644
--- a/tests/functional/parser/schemas.py
+++ b/tests/functional/parser/schemas.py
@@ -86,3 +86,8 @@ class MyCloudWatchBusiness(BaseModel):
class MyApiGatewayBusiness(BaseModel):
message: str
username: str
+
+
+class MyALambdaFuncUrlBusiness(BaseModel):
+ message: str
+ username: str
diff --git a/tests/functional/parser/test_lambda_function_url.py b/tests/functional/parser/test_lambda_function_url.py
new file mode 100644
index 00000000000..a63a4e25884
--- /dev/null
+++ b/tests/functional/parser/test_lambda_function_url.py
@@ -0,0 +1,128 @@
+from aws_lambda_powertools.utilities.parser import envelopes, event_parser
+from aws_lambda_powertools.utilities.parser.models import LambdaFunctionUrlModel
+from aws_lambda_powertools.utilities.typing import LambdaContext
+from tests.functional.parser.schemas import MyALambdaFuncUrlBusiness
+from tests.functional.utils import load_event
+
+
+@event_parser(model=MyALambdaFuncUrlBusiness, envelope=envelopes.LambdaFunctionUrlEnvelope)
+def handle_lambda_func_url_with_envelope(event: MyALambdaFuncUrlBusiness, _: LambdaContext):
+ assert event.message == "Hello"
+ assert event.username == "Ran"
+
+
+@event_parser(model=LambdaFunctionUrlModel)
+def handle_lambda_func_url_event(event: LambdaFunctionUrlModel, _: LambdaContext):
+ return event
+
+
+def test_lambda_func_url_event_with_envelope():
+ event = load_event("lambdaFunctionUrlEvent.json")
+ event["body"] = '{"message": "Hello", "username": "Ran"}'
+ handle_lambda_func_url_with_envelope(event, LambdaContext())
+
+
+def test_lambda_function_url_event():
+ json_event = load_event("lambdaFunctionUrlEvent.json")
+ event: LambdaFunctionUrlModel = handle_lambda_func_url_event(json_event, LambdaContext())
+
+ assert event.version == "2.0"
+ assert event.routeKey == "$default"
+
+ assert event.rawQueryString == ""
+
+ assert event.cookies is None
+
+ headers = event.headers
+ assert len(headers) == 20
+
+ assert event.queryStringParameters is None
+
+ assert event.isBase64Encoded is False
+ assert event.body is None
+ assert event.pathParameters is None
+ assert event.stageVariables is None
+
+ request_context = event.requestContext
+
+ assert request_context.accountId == "anonymous"
+ assert request_context.apiId is not None
+ assert request_context.domainName == ".lambda-url.us-east-1.on.aws"
+ assert request_context.domainPrefix == ""
+ assert request_context.requestId == "id"
+ assert request_context.routeKey == "$default"
+ assert request_context.stage == "$default"
+ assert request_context.time is not None
+ convert_time = int(round(request_context.timeEpoch.timestamp() * 1000))
+ assert convert_time == 1659687279885
+ assert request_context.authorizer is None
+
+ http = request_context.http
+ assert http.method == "GET"
+ assert http.path == "/"
+ assert http.protocol == "HTTP/1.1"
+ assert str(http.sourceIp) == "123.123.123.123/32"
+ assert http.userAgent == "agent"
+
+ assert request_context.authorizer is None
+
+
+def test_lambda_function_url_event_iam():
+ json_event = load_event("lambdaFunctionUrlIAMEvent.json")
+ event: LambdaFunctionUrlModel = handle_lambda_func_url_event(json_event, LambdaContext())
+
+ assert event.version == "2.0"
+ assert event.routeKey == "$default"
+
+ assert event.rawQueryString == "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.queryStringParameters
+ assert len(query_string_parameters) == 2
+ assert query_string_parameters.get("parameter2") == "value"
+
+ assert event.isBase64Encoded is False
+ assert event.body == "Hello from client!"
+ assert event.pathParameters is None
+ assert event.stageVariables is None
+
+ request_context = event.requestContext
+
+ assert request_context.accountId == "123456789012"
+ assert request_context.apiId is not None
+ assert request_context.domainName == ".lambda-url.us-west-2.on.aws"
+ assert request_context.domainPrefix == ""
+ assert request_context.requestId == "id"
+ assert request_context.routeKey == "$default"
+ assert request_context.stage == "$default"
+ assert request_context.time is not None
+ convert_time = int(round(request_context.timeEpoch.timestamp() * 1000))
+ assert convert_time == 1583348638390
+
+ http = request_context.http
+ assert http.method == "POST"
+ assert http.path == "/my/path"
+ assert http.protocol == "HTTP/1.1"
+ assert str(http.sourceIp) == "123.123.123.123/32"
+ assert http.userAgent == "agent"
+
+ authorizer = request_context.authorizer
+ assert authorizer is not None
+ assert authorizer.jwt is None
+ assert authorizer.lambda_value is None
+
+ iam = authorizer.iam
+ assert iam is not None
+ assert iam.accessKey == "AKIA..."
+ assert iam.accountId == "111122223333"
+ assert iam.callerId == "AIDA..."
+ assert iam.cognitoIdentity is None
+ assert iam.principalOrgId is None
+ assert iam.userId == "AIDA..."
+ assert iam.userArn == "arn:aws:iam::111122223333:user/example-user"