Skip to content

feat(event_handlers): Add support for Lambda Function URLs #1408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
2 changes: 2 additions & 0 deletions aws_lambda_powertools/event_handler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Response,
)
from .appsync import AppSyncResolver
from .lambda_function_url import LambdaFunctionUrlResolver

__all__ = [
"AppSyncResolver",
Expand All @@ -19,5 +20,6 @@
"ALBResolver",
"ApiGatewayResolver",
"CORSConfig",
"LambdaFunctionUrlResolver",
"Response",
]
11 changes: 10 additions & 1 deletion aws_lambda_powertools/event_handler/api_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -36,6 +41,7 @@ class ProxyEventType(Enum):
APIGatewayProxyEvent = "APIGatewayProxyEvent"
APIGatewayProxyEventV2 = "APIGatewayProxyEventV2"
ALBEvent = "ALBEvent"
LambdaFunctionUrlEvent = "LambdaFunctionUrlEvent"


class CORSConfig:
Expand Down Expand Up @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to future self: we should improve the return type to a more accurate one with @overload.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to future self: wait for python 3.10 to actually have proper pattern matching

logger.debug("Converting event to Lambda Function URL contract")
return LambdaFunctionUrlEvent(event)
logger.debug("Converting event to ALB contract")
return ALBEvent(event)

Expand Down
53 changes: 53 additions & 0 deletions aws_lambda_powertools/event_handler/lambda_function_url.py
Original file line number Diff line number Diff line change
@@ -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:
-----
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 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["value"]}

@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)
1 change: 1 addition & 0 deletions aws_lambda_powertools/logging/correlation_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions aws_lambda_powertools/utilities/data_classes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,6 +29,7 @@
"DynamoDBStreamEvent",
"EventBridgeEvent",
"KinesisStreamEvent",
"LambdaFunctionUrlEvent",
"S3Event",
"SESEvent",
"SNSEvent",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand All @@ -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]]:
Expand Down
3 changes: 2 additions & 1 deletion aws_lambda_powertools/utilities/data_classes/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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:
-----
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
"""

pass
47 changes: 36 additions & 11 deletions docs/core/event_handler/api_gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -18,23 +18,31 @@ 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

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`.
For resolvers, we provide: `APIGatewayRestResolver`, `APIGatewayHttpResolver`, `ALBResolver`, and `LambdaFunctionUrlResolver` .

???+ info
We will use `APIGatewayRestResolver` as the default across examples.
Expand Down Expand Up @@ -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/<todo_id>` to configure dynamic URL paths, where `<todo_id>` will be resolved at runtime.
Expand Down Expand Up @@ -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).

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch!!

* 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.
Expand Down
Loading