Skip to content

Commit 5dfc459

Browse files
authored
feat(event_handlers): Add support for Lambda Function URLs (#1408)
1 parent 16221e0 commit 5dfc459

File tree

17 files changed

+438
-36
lines changed

17 files changed

+438
-36
lines changed

aws_lambda_powertools/event_handler/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
Response,
1212
)
1313
from .appsync import AppSyncResolver
14+
from .lambda_function_url import LambdaFunctionUrlResolver
1415

1516
__all__ = [
1617
"AppSyncResolver",
@@ -19,5 +20,6 @@
1920
"ALBResolver",
2021
"ApiGatewayResolver",
2122
"CORSConfig",
23+
"LambdaFunctionUrlResolver",
2224
"Response",
2325
]

aws_lambda_powertools/event_handler/api_gateway.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
from aws_lambda_powertools.shared import constants
1818
from aws_lambda_powertools.shared.functions import resolve_truthy_env_var_choice
1919
from aws_lambda_powertools.shared.json_encoder import Encoder
20-
from aws_lambda_powertools.utilities.data_classes import ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2
20+
from aws_lambda_powertools.utilities.data_classes import (
21+
ALBEvent,
22+
APIGatewayProxyEvent,
23+
APIGatewayProxyEventV2,
24+
LambdaFunctionUrlEvent,
25+
)
2126
from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent
2227
from aws_lambda_powertools.utilities.typing import LambdaContext
2328

@@ -36,6 +41,7 @@ class ProxyEventType(Enum):
3641
APIGatewayProxyEvent = "APIGatewayProxyEvent"
3742
APIGatewayProxyEventV2 = "APIGatewayProxyEventV2"
3843
ALBEvent = "ALBEvent"
44+
LambdaFunctionUrlEvent = "LambdaFunctionUrlEvent"
3945

4046

4147
class CORSConfig:
@@ -546,6 +552,9 @@ def _to_proxy_event(self, event: Dict) -> BaseProxyEvent:
546552
if self._proxy_type == ProxyEventType.APIGatewayProxyEventV2:
547553
logger.debug("Converting event to API Gateway HTTP API contract")
548554
return APIGatewayProxyEventV2(event)
555+
if self._proxy_type == ProxyEventType.LambdaFunctionUrlEvent:
556+
logger.debug("Converting event to Lambda Function URL contract")
557+
return LambdaFunctionUrlEvent(event)
549558
logger.debug("Converting event to ALB contract")
550559
return ALBEvent(event)
551560

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import Callable, Dict, List, Optional
2+
3+
from aws_lambda_powertools.event_handler import CORSConfig
4+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, ProxyEventType
5+
from aws_lambda_powertools.utilities.data_classes import LambdaFunctionUrlEvent
6+
7+
8+
class LambdaFunctionUrlResolver(ApiGatewayResolver):
9+
"""AWS Lambda Function URL resolver
10+
11+
Notes:
12+
-----
13+
Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0.
14+
15+
Documentation:
16+
- https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html
17+
- https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads
18+
19+
Examples
20+
--------
21+
Simple example integrating with Tracer
22+
23+
```python
24+
from aws_lambda_powertools import Tracer
25+
from aws_lambda_powertools.event_handler import LambdaFunctionUrlResolver
26+
27+
tracer = Tracer()
28+
app = LambdaFunctionUrlResolver()
29+
30+
@app.get("/get-call")
31+
def simple_get():
32+
return {"message": "Foo"}
33+
34+
@app.post("/post-call")
35+
def simple_post():
36+
post_data: dict = app.current_event.json_body
37+
return {"message": post_data}
38+
39+
@tracer.capture_lambda_handler
40+
def lambda_handler(event, context):
41+
return app.resolve(event, context)
42+
"""
43+
44+
current_event: LambdaFunctionUrlEvent
45+
46+
def __init__(
47+
self,
48+
cors: Optional[CORSConfig] = None,
49+
debug: Optional[bool] = None,
50+
serializer: Optional[Callable[[Dict], str]] = None,
51+
strip_prefixes: Optional[List[str]] = None,
52+
):
53+
super().__init__(ProxyEventType.LambdaFunctionUrlEvent, cors, debug, serializer, strip_prefixes)

aws_lambda_powertools/logging/correlation_paths.py

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
APPSYNC_RESOLVER = 'request.headers."x-amzn-trace-id"'
77
APPLICATION_LOAD_BALANCER = 'headers."x-amzn-trace-id"'
88
EVENT_BRIDGE = "id"
9+
LAMBDA_FUNCTION_URL = API_GATEWAY_REST
910
S3_OBJECT_LAMBDA = "xAmzRequestId"

aws_lambda_powertools/utilities/data_classes/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .event_bridge_event import EventBridgeEvent
1313
from .event_source import event_source
1414
from .kinesis_stream_event import KinesisStreamEvent
15+
from .lambda_function_url_event import LambdaFunctionUrlEvent
1516
from .s3_event import S3Event
1617
from .ses_event import SESEvent
1718
from .sns_event import SNSEvent
@@ -28,6 +29,7 @@
2829
"DynamoDBStreamEvent",
2930
"EventBridgeEvent",
3031
"KinesisStreamEvent",
32+
"LambdaFunctionUrlEvent",
3133
"S3Event",
3234
"SESEvent",
3335
"SNSEvent",

aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,22 @@ def caller_id(self) -> Optional[str]:
127127
def cognito_amr(self) -> Optional[List[str]]:
128128
"""This represents how the user was authenticated.
129129
AMR stands for Authentication Methods References as per the openid spec"""
130-
return self["cognitoIdentity"].get("amr")
130+
cognito_identity = self["cognitoIdentity"] or {} # not available in FunctionURL
131+
return cognito_identity.get("amr")
131132

132133
@property
133134
def cognito_identity_id(self) -> Optional[str]:
134135
"""The Amazon Cognito identity ID of the caller making the request.
135136
Available only if the request was signed with Amazon Cognito credentials."""
136-
return self["cognitoIdentity"].get("identityId")
137+
cognito_identity = self.get("cognitoIdentity") or {} # not available in FunctionURL
138+
return cognito_identity.get("identityId")
137139

138140
@property
139141
def cognito_identity_pool_id(self) -> Optional[str]:
140142
"""The Amazon Cognito identity pool ID of the caller making the request.
141143
Available only if the request was signed with Amazon Cognito credentials."""
142-
return self["cognitoIdentity"].get("identityPoolId")
144+
cognito_identity = self.get("cognitoIdentity") or {} # not available in FunctionURL
145+
return cognito_identity.get("identityPoolId")
143146

144147
@property
145148
def principal_org_id(self) -> Optional[str]:
@@ -159,12 +162,14 @@ def user_id(self) -> Optional[str]:
159162

160163
class RequestContextV2Authorizer(DictWrapper):
161164
@property
162-
def jwt_claim(self) -> Dict[str, Any]:
163-
return self["jwt"]["claims"]
165+
def jwt_claim(self) -> Optional[Dict[str, Any]]:
166+
jwt = self.get("jwt") or {} # not available in FunctionURL
167+
return jwt.get("claims")
164168

165169
@property
166-
def jwt_scopes(self) -> List[str]:
167-
return self["jwt"]["scopes"]
170+
def jwt_scopes(self) -> Optional[List[str]]:
171+
jwt = self.get("jwt") or {} # not available in FunctionURL
172+
return jwt.get("scopes")
168173

169174
@property
170175
def get_lambda(self) -> Optional[Dict[str, Any]]:

aws_lambda_powertools/utilities/data_classes/common.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -395,5 +395,7 @@ def time_epoch(self) -> int:
395395
@property
396396
def authentication(self) -> Optional[RequestContextClientCert]:
397397
"""Optional when using mutual TLS authentication"""
398-
client_cert = self["requestContext"].get("authentication", {}).get("clientCert")
398+
# FunctionURL might have NONE as AuthZ
399+
authentication = self["requestContext"].get("authentication") or {}
400+
client_cert = authentication.get("clientCert")
399401
return None if client_cert is None else RequestContextClientCert(client_cert)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from aws_lambda_powertools.utilities.data_classes.api_gateway_proxy_event import APIGatewayProxyEventV2
2+
3+
4+
class LambdaFunctionUrlEvent(APIGatewayProxyEventV2):
5+
"""AWS Lambda Function URL event
6+
7+
Notes:
8+
-----
9+
Lambda Function URL follows the API Gateway HTTP APIs Payload Format Version 2.0.
10+
11+
Keys related to API Gateway features not available in Function URL use a sentinel value (e.g.`routeKey`, `stage`).
12+
13+
Documentation:
14+
- https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html
15+
- https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads
16+
"""
17+
18+
pass

docs/core/event_handler/api_gateway.md

+37-11
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ title: REST API
33
description: Core utility
44
---
55

6-
Event handler for Amazon API Gateway REST and HTTP APIs, and Application Loader Balancer (ALB).
6+
Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Balancer (ALB), and Lambda Function URLs.
77

88
## Key Features
99

10-
* Lightweight routing to reduce boilerplate for API Gateway REST/HTTP API and ALB
10+
* Lightweight routing to reduce boilerplate for API Gateway REST/HTTP API, ALB and Lambda Function URLs.
1111
* Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer
1212
* Built-in integration with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"} for self-documented event schema
1313

@@ -18,23 +18,31 @@ Event handler for Amazon API Gateway REST and HTTP APIs, and Application Loader
1818

1919
### Required resources
2020

21-
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.
21+
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.
2222

23-
This is the sample infrastructure for API Gateway we are using for the examples in this documentation.
23+
This is the sample infrastructure for API Gateway and Lambda Function URLs we are using for the examples in this documentation.
2424

2525
???+ info "There is no additional permissions or dependencies required to use this utility."
2626

27-
```yaml title="AWS Serverless Application Model (SAM) example"
28-
--8<-- "examples/event_handler_rest/sam/template.yaml"
29-
```
27+
=== "API Gateway SAM Template"
28+
29+
```yaml title="AWS Serverless Application Model (SAM) example"
30+
--8<-- "examples/event_handler_rest/sam/template.yaml"
31+
```
32+
33+
=== "Lambda Function URL SAM Template"
34+
35+
```yaml title="AWS Serverless Application Model (SAM) example"
36+
--8<-- "examples/event_handler_lambda_function_url/sam/template.yaml"
37+
```
3038

3139
### Event Resolvers
3240

3341
Before you decorate your functions to handle a given path and HTTP method(s), you need to initialize a resolver.
3442

3543
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.
3644

37-
For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, and `ALBResolver`.
45+
For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, `ALBResolver`, and `LambdaFunctionUrlResolver` .
3846

3947
???+ info
4048
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
8795
--8<-- "examples/event_handler_rest/src/getting_started_alb_api_resolver.py"
8896
```
8997

98+
#### Lambda Function URL
99+
100+
When using [AWS Lambda Function URL](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), you can use `LambdaFunctionUrlResolver`.
101+
102+
=== "getting_started_lambda_function_url_resolver.py"
103+
104+
```python hl_lines="5 11" title="Using Lambda Function URL resolver"
105+
--8<-- "examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.py"
106+
```
107+
108+
=== "getting_started_lambda_function_url_resolver.json"
109+
110+
```json hl_lines="4-5" title="Example payload delivered to the handler"
111+
--8<-- "examples/event_handler_lambda_function_url/src/getting_started_lambda_function_url_resolver.json"
112+
```
113+
90114
### Dynamic routes
91115

92116
You can use `/todos/<todo_id>` to configure dynamic URL paths, where `<todo_id>` will be resolved at runtime.
@@ -270,7 +294,7 @@ This will ensure that CORS headers are always returned as part of the response w
270294

271295
#### Pre-flight
272296

273-
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.
297+
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.
274298

275299
For convenience, we automatically handle that for you as long as you [setup CORS in the constructor level](#cors).
276300

@@ -339,6 +363,8 @@ Like `compress` feature, the client must send the `Accept` header with the corre
339363
???+ warning
340364
This feature requires API Gateway to configure binary media types, see [our sample infrastructure](#required-resources) for reference.
341365

366+
???+ note
367+
Lambda Function URLs handle binary media types automatically.
342368
=== "binary_responses.py"
343369

344370
```python hl_lines="14 20"
@@ -380,7 +406,7 @@ This will enable full tracebacks errors in the response, print request and respo
380406

381407
### Custom serializer
382408

383-
You can instruct API Gateway handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing.
409+
You can instruct event handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing.
384410

385411
```python hl_lines="35 40" title="Using a custom JSON serializer for responses"
386412
--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
501527

502528
**Downsides**
503529

504-
* **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.
530+
* **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.
505531
* 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.
506532
* **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,
507533
your development, building, deployment tooling need to accommodate the distinct layout.

0 commit comments

Comments
 (0)