diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 9dba4219a95..a99394b10f7 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -1,5 +1,6 @@ import base64 import json +import logging import re import zlib from enum import Enum @@ -10,6 +11,8 @@ from aws_lambda_powertools.utilities.data_classes.common import BaseProxyEvent from aws_lambda_powertools.utilities.typing import LambdaContext +logger = logging.getLogger(__name__) + class ProxyEventType(Enum): """An enumerations of the supported proxy event types.""" @@ -28,37 +31,37 @@ class CORSConfig(object): Simple cors example using the default permissive cors, not this should only be used during early prototyping - >>> from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver - >>> - >>> app = ApiGatewayResolver() - >>> - >>> @app.get("/my/path", cors=True) - >>> def with_cors(): - >>> return {"message": "Foo"} + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver + + app = ApiGatewayResolver() + + @app.get("/my/path", cors=True) + def with_cors(): + return {"message": "Foo"} Using a custom CORSConfig where `with_cors` used the custom provided CORSConfig and `without_cors` do not include any cors headers. - >>> from aws_lambda_powertools.event_handler.api_gateway import ( - >>> ApiGatewayResolver, CORSConfig - >>> ) - >>> - >>> cors_config = CORSConfig( - >>> allow_origin="https://wwww.example.com/", - >>> expose_headers=["x-exposed-response-header"], - >>> allow_headers=["x-custom-request-header"], - >>> max_age=100, - >>> allow_credentials=True, - >>> ) - >>> app = ApiGatewayResolver(cors=cors_config) - >>> - >>> @app.get("/my/path", cors=True) - >>> def with_cors(): - >>> return {"message": "Foo"} - >>> - >>> @app.get("/another-one") - >>> def without_cors(): - >>> return {"message": "Foo"} + from aws_lambda_powertools.event_handler.api_gateway import ( + ApiGatewayResolver, CORSConfig + ) + + cors_config = CORSConfig( + allow_origin="https://wwww.example.com/", + expose_headers=["x-exposed-response-header"], + allow_headers=["x-custom-request-header"], + max_age=100, + allow_credentials=True, + ) + app = ApiGatewayResolver(cors=cors_config) + + @app.get("/my/path", cors=True) + def with_cors(): + return {"message": "Foo"} + + @app.get("/another-one") + def without_cors(): + return {"message": "Foo"} """ _REQUIRED_HEADERS = ["Authorization", "Content-Type", "X-Amz-Date", "X-Api-Key", "X-Amz-Security-Token"] @@ -66,9 +69,9 @@ class CORSConfig(object): def __init__( self, allow_origin: str = "*", - allow_headers: List[str] = None, - expose_headers: List[str] = None, - max_age: int = None, + allow_headers: Optional[List[str]] = None, + expose_headers: Optional[List[str]] = None, + max_age: Optional[int] = None, allow_credentials: bool = False, ): """ @@ -77,13 +80,13 @@ def __init__( allow_origin: str The value of the `Access-Control-Allow-Origin` to send in the response. Defaults to "*", but should only be used during development. - allow_headers: str + allow_headers: Optional[List[str]] The list of additional allowed headers. This list is added to list of built in allowed headers: `Authorization`, `Content-Type`, `X-Amz-Date`, `X-Api-Key`, `X-Amz-Security-Token`. - expose_headers: str + expose_headers: Optional[List[str]] A list of values to return for the Access-Control-Expose-Headers - max_age: int + max_age: Optional[int] The value for the `Access-Control-Max-Age` allow_credentials: bool A boolean value that sets the value of `Access-Control-Allow-Credentials` @@ -170,6 +173,7 @@ def _compress(self): """Compress the response body, but only if `Accept-Encoding` headers includes gzip.""" self.response.headers["Content-Encoding"] = "gzip" if isinstance(self.response.body, str): + logger.debug("Converting string response to bytes before compressing it") self.response.body = bytes(self.response.body, "utf-8") gzip = zlib.compressobj(9, zlib.DEFLATED, zlib.MAX_WBITS | 16) self.response.body = gzip.compress(self.response.body) + gzip.flush() @@ -190,6 +194,7 @@ def build(self, event: BaseProxyEvent, cors: CORSConfig = None) -> Dict[str, Any self._route(event, cors) if isinstance(self.response.body, bytes): + logger.debug("Encoding bytes response with base64") self.response.base64_encoded = True self.response.body = base64.b64encode(self.response.body).decode() return { @@ -207,27 +212,26 @@ class ApiGatewayResolver: -------- Simple example with a custom lambda handler using the Tracer capture_lambda_handler decorator - >>> from aws_lambda_powertools import Tracer - >>> from aws_lambda_powertools.event_handler.api_gateway import ( - >>> ApiGatewayResolver - >>> ) - >>> - >>> tracer = Tracer() - >>> app = ApiGatewayResolver() - >>> - >>> @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) + ```python + from aws_lambda_powertools import Tracer + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver + + tracer = Tracer() + app = ApiGatewayResolver() + @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: BaseProxyEvent @@ -247,32 +251,144 @@ def __init__(self, proxy_type: Enum = ProxyEventType.APIGatewayProxyEvent, cors: self._cors = cors self._cors_methods: Set[str] = {"OPTIONS"} - def get(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None): - """Get route decorator with GET `method`""" + def get(self, rule: str, cors: bool = True, compress: bool = False, cache_control: str = None): + """Get route decorator with GET `method` + + 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.api_gateway import ApiGatewayResolver + + tracer = Tracer() + app = ApiGatewayResolver() + + @app.get("/get-call") + def simple_get(): + return {"message": "Foo"} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ return self.route(rule, "GET", cors, compress, cache_control) - def post(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None): - """Post route decorator with POST `method`""" + def post(self, rule: str, cors: bool = True, compress: bool = False, cache_control: str = None): + """Post route decorator with POST `method` + + 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.api_gateway import ApiGatewayResolver + + tracer = Tracer() + app = ApiGatewayResolver() + + @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) + ``` + """ return self.route(rule, "POST", cors, compress, cache_control) - def put(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None): - """Put route decorator with PUT `method`""" + def put(self, rule: str, cors: bool = True, compress: bool = False, cache_control: str = None): + """Put route decorator with PUT `method` + + 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.api_gateway import ApiGatewayResolver + + tracer = Tracer() + app = ApiGatewayResolver() + + @app.put("/put-call") + def simple_post(): + put_data: dict = app.current_event.json_body + return {"message": put_data["value"]} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ return self.route(rule, "PUT", cors, compress, cache_control) - def delete(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None): - """Delete route decorator with DELETE `method`""" + def delete(self, rule: str, cors: bool = True, compress: bool = False, cache_control: str = None): + """Delete route decorator with DELETE `method` + + 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.api_gateway import ApiGatewayResolver + + tracer = Tracer() + app = ApiGatewayResolver() + + @app.delete("/delete-call") + def simple_delete(): + return {"message": "deleted"} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ return self.route(rule, "DELETE", cors, compress, cache_control) - def patch(self, rule: str, cors: bool = False, compress: bool = False, cache_control: str = None): - """Patch route decorator with PATCH `method`""" + def patch(self, rule: str, cors: bool = True, compress: bool = False, cache_control: str = None): + """Patch route decorator with PATCH `method` + + 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.api_gateway import ApiGatewayResolver + + tracer = Tracer() + app = ApiGatewayResolver() + + @app.patch("/patch-call") + def simple_patch(): + patch_data: dict = app.current_event.json_body + patch_data["value"] = patched + + return {"message": patch_data} + + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + """ return self.route(rule, "PATCH", cors, compress, cache_control) - def route(self, rule: str, method: str, cors: bool = False, compress: bool = False, cache_control: str = None): + def route(self, rule: str, method: str, cors: bool = True, compress: bool = False, cache_control: str = None): """Route decorator includes parameter `method`""" def register_resolver(func: Callable): + logger.debug(f"Adding route using rule {rule} and method {method.upper()}") self._routes.append(Route(method, self._compile_regex(rule), func, cors, compress, cache_control)) if cors: + logger.debug(f"Registering method {method.upper()} to Allow Methods in CORS") self._cors_methods.add(method.upper()) return func @@ -308,9 +424,12 @@ def _compile_regex(rule: str): def _to_proxy_event(self, event: Dict) -> BaseProxyEvent: """Convert the event dict to the corresponding data class""" if self._proxy_type == ProxyEventType.APIGatewayProxyEvent: + logger.debug("Converting event to API Gateway REST API contract") return APIGatewayProxyEvent(event) if self._proxy_type == ProxyEventType.APIGatewayProxyEventV2: + logger.debug("Converting event to API Gateway HTTP API contract") return APIGatewayProxyEventV2(event) + logger.debug("Converting event to ALB contract") return ALBEvent(event) def _resolve(self) -> ResponseBuilder: @@ -322,17 +441,21 @@ def _resolve(self) -> ResponseBuilder: continue match: Optional[re.Match] = route.rule.match(path) if match: + logger.debug("Found a registered route. Calling function") return self._call_route(route, match.groupdict()) + logger.debug(f"No match found for path {path} and method {method}") return self._not_found(method) def _not_found(self, method: str) -> ResponseBuilder: """Called when no matching route was found and includes support for the cors preflight response""" headers = {} if self._cors: + logger.debug("CORS is enabled, updating headers.") headers.update(self._cors.to_dict()) - if method == "OPTIONS": # Preflight + if method == "OPTIONS": # Pre-flight + logger.debug("Pre-flight request detected. Returning CORS with null response") headers["Access-Control-Allow-Methods"] = ",".join(sorted(self._cors_methods)) return ResponseBuilder(Response(status_code=204, content_type=None, headers=headers, body=None)) @@ -361,11 +484,10 @@ def _to_response(result: Union[Dict, Response]) -> Response: """ if isinstance(result, Response): return result - elif isinstance(result, dict): - return Response( - status_code=200, - content_type="application/json", - body=json.dumps(result, separators=(",", ":"), cls=Encoder), - ) - else: # Tuple[int, str, Union[bytes, str]] - return Response(*result) + + logger.debug("Simple response detected, serializing return before constructing final response") + return Response( + status_code=200, + content_type="application/json", + body=json.dumps(result, separators=(",", ":"), cls=Encoder), + ) diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md index 860a9918e47..8551f0b3cf6 100644 --- a/docs/core/event_handler/api_gateway.md +++ b/docs/core/event_handler/api_gateway.md @@ -3,319 +3,711 @@ title: API Gateway description: Core utility --- -Event handler for AWS API Gateway and Application Loader Balancers. +Event handler for Amazon API Gateway REST/HTTP APIs and Application Loader Balancer (ALB). + +!!! info "This is currently in Beta as we want to hear feedback on UX." ### Key Features -* Routes - `@app.get("/foo")` -* Path expressions - `@app.delete("/delete/")` -* Cors - `@app.post("/make_foo", cors=True)` or via `CORSConfig` and builtin CORS preflight route -* Base64 encode binary - `@app.get("/logo.png")` -* Gzip Compression - `@app.get("/large-json", compress=True)` -* Cache-control - `@app.get("/foo", cache_control="max-age=600")` -* Rest API simplification with function returns a Dict -* Support function returns a Response object which give fine-grained control of the headers -* JSON encoding of Decimals +* Lightweight routing to reduce boilerplate for API Gateway REST/HTTP API and ALB +* Seamless support for CORS, binary and Gzip compression +* Integrates with [Data classes utilities](../../utilities/data_classes.md){target="_blank"} to easily access event and identity information +* Built-in support for Decimals JSON encoding +* Support for dynamic path expressions + +## Getting started + +### 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. There is no additional permissions or dependencies required to use this utility. + +This is the sample infrastructure for API Gateway we are using for the examples in this documentation. + +=== "template.yml" + + ```yaml + AWSTemplateFormatVersion: '2010-09-09' + Transform: AWS::Serverless-2016-10-31 + Description: Hello world event handler API Gateway + + Globals: + Api: + TracingEnabled: true + Cors: # see CORS section + AllowOrigin: "'https://example.com'" + AllowHeaders: "'Content-Type,Authorization,X-Amz-Date'" + MaxAge: "'300'" + BinaryMediaTypes: # see Binary responses section + - '*~1*' # converts to */* for any binary type + Function: + Timeout: 5 + Runtime: python3.8 + Tracing: Active + Environment: + Variables: + LOG_LEVEL: INFO + POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 + POWERTOOLS_LOGGER_LOG_EVENT: true + POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication + POWERTOOLS_SERVICE_NAME: hello + + Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Handler: app.lambda_handler + CodeUri: hello_world + Description: Hello World function + Events: + HelloUniverse: + Type: Api + Properties: + Path: /hello + Method: GET + HelloYou: + Type: Api + Properties: + Path: /hello/{name} # see Dynamic routes section + Method: GET + CustomMessage: + Type: Api + Properties: + Path: /{message}/{name} # see Dynamic routes section + Method: GET + + Outputs: + HelloWorldApigwURL: + Description: "API Gateway endpoint URL for Prod environment for Hello World Function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello" + + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + ``` + +### API Gateway decorator + +You can define your functions to match a path and HTTP method, when you use the decorator `ApiGatewayResolver`. + +Here's an example where we have two separate functions to resolve two paths: `/hello`. + +!!! info "We automatically serialize `Dict` responses as JSON and set content-type to `application/json`" + +=== "app.py" + + ```python hl_lines="3 7 9 12 18" + from aws_lambda_powertools import Logger, Tracer + from aws_lambda_powertools.logging import correlation_paths + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver + + tracer = Tracer() + logger = Logger() + app = ApiGatewayResolver() # by default API Gateway REST API (v1) + + @app.get("/hello") + @tracer.capture_method + def get_hello_universe(): + return {"message": "hello universe"} + + # You can continue to use other utilities just as before + @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` +=== "hello_event.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" + { + "body": "hello", + "resource": "/hello", + "path": "/hello", + "httpMethod": "GET", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "multiValueQueryStringParameters": {}, + "pathParameters": { + "hello": "/hello" + }, + "stageVariables": {}, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": {}, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "Prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "25/Jul/2020:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/Prod/hello", + "resourcePath": "/hello", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } + } + ``` + +=== "response.json" + + ```json + { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"message\":\"hello universe\"}", + "isBase64Encoded": false + } + ``` + +#### HTTP API + +When using API Gateway HTTP API to front your Lambda functions, you can instruct `ApiGatewayResolver` to conform with their contract via `proxy_type` param: + +=== "app.py" + + ```python hl_lines="3 7" + from aws_lambda_powertools import Logger, Tracer + from aws_lambda_powertools.logging import correlation_paths + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, ProxyEventType + + tracer = Tracer() + logger = Logger() + app = ApiGatewayResolver(proxy_type=ProxyEventType.http_api_v2) + + @app.get("/hello") + @tracer.capture_method + def get_hello_universe(): + return {"message": "hello universe"} -## Examples + # You can continue to use other utilities just as before + @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP) + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` -> TODO - Break on into smaller examples +#### ALB -### All in one example +When using ALB to front your Lambda functions, you can instruct `ApiGatewayResolver` to conform with their contract via `proxy_type` param: === "app.py" -```python -from decimal import Decimal -import json -from typing import Dict, Tuple - -from aws_lambda_powertools import Tracer -from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEvent -from aws_lambda_powertools.event_handler.api_gateway import ( - ApiGatewayResolver, - CORSConfig, - ProxyEventType, - Response, -) - -tracer = Tracer() -# Other supported proxy_types: "APIGatewayProxyEvent", "APIGatewayProxyEventV2", "ALBEvent" -app = ApiGatewayResolver( - proxy_type=ProxyEventType.APIGatewayProxyEvent, - cors=CORSConfig( - allow_origin="https://www.example.com/", - expose_headers=["x-exposed-response-header"], - allow_headers=["x-custom-request-header"], - max_age=100, - allow_credentials=True, - ) -) - - -@app.get("/foo", compress=True) -def get_foo() -> Tuple[int, str, str]: - # Matches on http GET and proxy path "/foo" - # and return status code: 200, content-type: text/html and body: Hello - return 200, "text/html", "Hello" - - -@app.get("/logo.png") -def get_logo() -> Tuple[int, str, bytes]: - # Base64 encodes the return bytes body automatically - logo: bytes = load_logo() - return 200, "image/png", logo - - -@app.post("/make_foo", cors=True) -def make_foo() -> Tuple[int, str, str]: - # Matches on http POST and proxy path "/make_foo" - post_data: dict = app.current_event.json_body - return 200, "application/json", json.dumps(post_data["value"]) - - -@app.delete("/delete/") -def delete_foo(uid: str) -> Tuple[int, str, str]: - # Matches on http DELETE and proxy path starting with "/delete/" - assert isinstance(app.current_event, APIGatewayProxyEvent) - assert app.current_event.request_context.authorizer.claims is not None - assert app.current_event.request_context.authorizer.claims["username"] == "Mike" - return 200, "application/json", json.dumps({"id": uid}) - - -@app.get("/hello/") -def hello_user(username: str) -> Tuple[int, str, str]: - return 200, "text/html", f"Hello {username}!" - - -@app.get("/rest") -def rest_fun() -> Dict: - # Returns a statusCode: 200, Content-Type: application/json and json.dumps dict - # and handles the serialization of decimals to json string - return {"message": "Example", "second": Decimal("100.01")} - - -@app.get("/foo3") -def foo3() -> Response: - return Response( - status_code=200, - content_type="application/json", - headers={"custom-header": "value"}, - body=json.dumps({"message": "Foo3"}), - ) - - -@tracer.capture_lambda_handler -def lambda_handler(event, context) -> Dict: - return app.resolve(event, context) -``` - -### Compress examples + ```python hl_lines="3 7" + from aws_lambda_powertools import Logger, Tracer + from aws_lambda_powertools.logging import correlation_paths + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, ProxyEventType + + tracer = Tracer() + logger = Logger() + app = ApiGatewayResolver(proxy_type=ProxyEventType.alb_event) + + @app.get("/hello") + @tracer.capture_method + def get_hello_universe(): + return {"message": "hello universe"} + + # You can continue to use other utilities just as before + @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPLICATION_LOAD_BALANCER) + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + +### Dynamic routes + +You can use `/path/{dynamic_value}` when configuring dynamic URL paths. This allows you to define such dynamic value as part of your function signature. === "app.py" - ```python - from aws_lambda_powertools.event_handler.api_gateway import ( - ApiGatewayResolver - ) + ```python hl_lines="9 11" + from aws_lambda_powertools import Logger, Tracer + from aws_lambda_powertools.logging import correlation_paths + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver - app = ApiGatewayResolver() + tracer = Tracer() + logger = Logger() + app = ApiGatewayResolver() - @app.get("/foo", compress=True) - def get_foo() -> Tuple[int, str, str]: - # Matches on http GET and proxy path "/foo" - # and return status code: 200, content-type: text/html and body: Hello - return 200, "text/html", "Hello" - ``` + @app.get("/hello/") + @tracer.capture_method + def get_hello_you(name): + return {"message": f"hello {name}}"} -=== "GET /foo: request" - ```json - { - "headers": { - "Accept-Encoding": "gzip" - }, - "httpMethod": "GET", - "path": "/foo" - } - ``` + # You can continue to use other utilities just as before + @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` -=== "GET /foo: response" +=== "sample_request.json" - ```json + ```json { - "body": "H4sIAAAAAAACE/NIzcnJBwCCidH3BQAAAA==", - "headers": { - "Content-Encoding": "gzip", - "Content-Type": "text/html" - }, - "isBase64Encoded": true, - "statusCode": 200 + "resource": "/hello/{name}", + "path": "/hello/lessa", + "httpMethod": "GET", + ... } ``` -### CORS examples +You can also nest paths as configured earlier in [our sample infrastructure](#required-resources): `/{message}/{name}`. === "app.py" - ```python - from aws_lambda_powertools.event_handler.api_gateway import ( - ApiGatewayResolver, - CORSConfig, - ) - - app = ApiGatewayResolver( - proxy_type=ProxyEventType.http_api_v1, - cors=CORSConfig( - allow_origin="https://www.example.com/", - expose_headers=["x-exposed-response-header"], - allow_headers=["x-custom-request-header"], - max_age=100, - allow_credentials=True, - ) - ) - - @app.post("/make_foo", cors=True) - def make_foo() -> Tuple[int, str, str]: - # Matches on http POST and proxy path "/make_foo" - post_data: dict = app. current_event.json_body - return 200, "application/json", json.dumps(post_data["value"]) - ``` + ```python hl_lines="9 11" + from aws_lambda_powertools import Logger, Tracer + from aws_lambda_powertools.logging import correlation_paths + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver -=== "OPTIONS /make_foo" + tracer = Tracer() + logger = Logger() + app = ApiGatewayResolver() - ```json - { - "httpMethod": "OPTIONS", - "path": "/make_foo" - } - ``` + @app.get("//") + @tracer.capture_method + def get_message(message, name): + return {"message": f"{message}, {name}}"} -=== "<< OPTIONS /make_foo" + # You can continue to use other utilities just as before + @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` - ```json +=== "sample_request.json" + + ```json { - "body": null, - "headers": { - "Access-Control-Allow-Credentials": "true", - "Access-Control-Allow-Headers": "Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key,x-custom-request-header", - "Access-Control-Allow-Methods": "OPTIONS,POST", - "Access-Control-Allow-Origin": "https://www.example.com/", - "Access-Control-Expose-Headers": "x-exposed-response-header", - "Access-Control-Max-Age": "100" - }, - "isBase64Encoded": false, - "statusCode": 204 + "resource": "/{message}/{name}", + "path": "/hi/michael", + "httpMethod": "GET", + ... } ``` -=== "POST /make_foo" +### Accessing request details - ```json - { - "body": "{\"value\": \"Hello World\"}", - "httpMethod": "POST", - "path": "/make_foo" - } - ``` +By integrating with [Data classes utilities](../../utilities/data_classes.md){target="_blank"}, you have access to request details, Lambda context and also some convenient methods. + +These are made available in the response returned when instantiating `ApiGatewayResolver`, for example `app.current_event` and `app.lambda_context`. + +#### Query strings and payload + +Within `app.current_event` property, you can access query strings as dictionary via `query_string_parameters`, or by name 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. + +=== "app.py" + + ```python hl_lines="7-9 11" + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver + + app = ApiGatewayResolver() + + @app.get("/hello") + def get_hello_you(): + query_strings_as_dict = app.current_event.query_string_parameters + json_payload = app.current_event.json_body + payload = app.current_event.body + + name = app.current_event.get_query_string_value(name="name", default_value="") + return {"message": f"hello {name}}"} + + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + +#### Headers + +Similarly to [Query strings](#query-strings), you can access headers as dictionary via `app.current_event.headers`, or by name via `get_header_value`. + +=== "app.py" + + ```python hl_lines="7-8" + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver + + app = ApiGatewayResolver() + + @app.get("/hello") + def get_hello_you(): + headers_as_dict = app.current_event.headers + name = app.current_event.get_header_value(name="X-Name", default_value="") + + return {"message": f"hello {name}}"} + + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + +## Advanced + +### CORS + +You can configure CORS at the `ApiGatewayResolver` 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. + +=== "app.py" + + ```python hl_lines="9 11" + from aws_lambda_powertools import Logger, Tracer + from aws_lambda_powertools.logging import correlation_paths + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, CORSConfig + + tracer = Tracer() + logger = Logger() -=== "<< POST /make_foo" + cors_config = CORSConfig(allow_origin="https://example.com", max_age=300) + app = ApiGatewayResolver(cors=cors_config) + + @app.get("/hello/") + @tracer.capture_method + def get_hello_you(name): + return {"message": f"hello {name}}"} + + @app.get("/hello", cors=False) # optionally exclude CORS from response, if needed + @tracer.capture_method + def get_hello_no_cors_needed(): + return {"message": "hello, no CORS needed for this path ;)"} + + # You can continue to use other utilities just as before + @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) + @tracer.capture_lambda_handler + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + +=== "response.json" + + ```json + { + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "https://www.example.com", + "Access-Control-Allow-Headers": "Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key" + }, + "body": "{\"message\":\"hello lessa\"}", + "isBase64Encoded": false + } + ``` + +=== "response_no_cors.json" + + ```json + { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"message\":\"hello lessa\"}", + "isBase64Encoded": false + } + ``` + + +!!! tip "Optionally disable class on a per path basis with `cors=False` parameter" + +#### 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. + +=== "app.py" + + ```python hl_lines="10-14" + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, Response + + app = ApiGatewayResolver() + + @app.get("/hello") + def get_hello_you(): + payload = json.dumps({"message": "I'm a teapot"}) + custom_headers = {"X-Custom": "X-Value"} + + return Response(status_code=418, + content_type="application/json", + body=payload, + headers=custom_headers + ) + + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + +=== "response.json" ```json { - "body": "\"Hello World\"", + "body": "{\"message\":\"I\'m a teapot\"}", "headers": { - "Access-Control-Allow-Credentials": "true", - "Access-Control-Allow-Headers": "Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key,x-custom-request-header", - "Access-Control-Allow-Origin": "https://www.example.com/", - "Access-Control-Expose-Headers": "x-exposed-response-header", - "Access-Control-Max-Age": "100", - "Content-Type": "application/json" + "Content-Type": "application/json", + "X-Custom": "X-Value" }, "isBase64Encoded": false, - "statusCode": 200 + "statusCode": 418 } - ``` -### Simple rest example +### 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" === "app.py" - ```python - from aws_lambda_powertools.event_handler.api_gateway import ( - ApiGatewayResolver - ) + ```python hl_lines="5 7" + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver - app = ApiGatewayResolver() + app = ApiGatewayResolver() - @app.get("/rest") - def rest_fun() -> Dict: - # Returns a statusCode: 200, Content-Type: application/json and json.dumps dict - # and handles the serialization of decimals to json string - return {"message": "Example", "second": Decimal("100.01")} - ``` + @app.get("/hello", compress=True) + def get_hello_you(): + return {"message": "hello universe"} -=== "GET /rest: request" + def lambda_handler(event, context): + return app.resolve(event, context) + ``` - ```json +=== "sample_request.json" + + ```json { + "headers": { + "Accept-Encoding": "gzip" + }, "httpMethod": "GET", - "path": "/rest" + "path": "/hello", + ... } ``` -=== "GET /rest: response" +=== "response.json" ```json { - "body": "{\"message\":\"Example\",\"second\":\"100.01\"}", + "body": "H4sIAAAAAAACE6tWyk0tLk5MT1WyUspIzcnJVyjNyyxLLSpOVaoFANha8kEcAAAA", "headers": { + "Content-Encoding": "gzip", "Content-Type": "application/json" }, - "isBase64Encoded": false, + "isBase64Encoded": true, "statusCode": 200 } ``` -### Custom response +### Binary responses -=== "app.py" +For convenience, we automatically base64 encode binary responses. You can also use in combination with `compress` parameter if your client supports gzip. - ```python - from aws_lambda_powertools.event_handler.api_gateway import ( - ApiGatewayResolver - ) - - app = ApiGatewayResolver() - - @app.get("/foo3") - def foo3() -> Response: - return Response( - status_code=200, - content_type="application/json", - headers={"custom-header": "value"}, - body=json.dumps({"message": "Foo3"}), - ) - ``` +Like `compress` feature, the client must send the `Accept` header with the correct media type. -=== "GET /foo3: request" +!!! warning "This feature requires API Gateway to configure binary media types, see [our sample infrastructure](#required-resources) for reference" - ```json +=== "app.py" + + ```python hl_lines="4 7 11" + import os + from pathlib import Path + + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver, Response + + app = ApiGatewayResolver() + logo_file: bytes = Path(os.getenv("LAMBDA_TASK_ROOT") + "/logo.svg").read_bytes() + + @app.get("/logo") + def get_logo(): + return Response(status_code=200, content_type="image/svg+xml", body=logo_file) + + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + +=== "logo.svg" + ```xml + + + + + + + + + + + + + ``` +=== "sample_request.json" + + ```json { + "headers": { + "Accept": "image/svg+xml" + }, "httpMethod": "GET", - "path": "/foo3" + "path": "/logo", + ... } ``` -=== "GET /foo3: response" +=== "response.json" ```json { - "body": "{\"message\": \"Foo3\"}", + "body": "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", "headers": { - "Content-Type": "application/json", - "custom-header": "value" + "Content-Type": "image/svg+xml" }, - "isBase64Encoded": false, + "isBase64Encoded": true, "statusCode": 200 } ``` + +## Testing your code + +You can test your routes by passing a proxy event request where `path` and `httpMethod`. + +=== "test_app.py" + + ```python hl_lines="18-24" + from dataclasses import dataclass + + import pytest + import app + + @pytest.fixture + def lambda_context(): + @dataclass + class LambdaContext: + function_name: str = "test" + memory_limit_in_mb: int = 128 + invoked_function_arn: str = "arn:aws:lambda:eu-west-1:809313241:function:test" + aws_request_id: str = "52fdfc07-2182-154f-163f-5f0f9a621d72" + + return LambdaContext() + + def test_lambda_handler(lambda_context): + minimal_event = { + "path": "/hello", + "httpMethod": "GET" + "requestContext": { # correlation ID + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef" + } + } + + app.lambda_handler(minimal_event, lambda_context) + ``` + +=== "app.py" + + ```python + from aws_lambda_powertools import Logger + from aws_lambda_powertools.logging import correlation_paths + from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver + + logger = Logger() + app = ApiGatewayResolver() # by default API Gateway REST API (v1) + + @app.get("/hello") + def get_hello_universe(): + return {"message": "hello universe"} + + # You can continue to use other utilities just as before + @logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST) + def lambda_handler(event, context): + return app.resolve(event, context) + ``` + +## 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. diff --git a/docs/core/event_handler/appsync.md b/docs/core/event_handler/appsync.md index 3f61a4ad311..67ad1999285 100644 --- a/docs/core/event_handler/appsync.md +++ b/docs/core/event_handler/appsync.md @@ -7,8 +7,6 @@ Event handler for AWS AppSync Direct Lambda Resolver and Amplify GraphQL Transfo ### Key Features - - * Automatically parse API arguments to function arguments * Choose between strictly match a GraphQL field name or all of them to a function * Integrates with [Data classes utilities](../../utilities/data_classes.md){target="_blank"} to access resolver and identity information diff --git a/docs/core/logger.md b/docs/core/logger.md index f8e806aa6b4..a544bf91e4b 100644 --- a/docs/core/logger.md +++ b/docs/core/logger.md @@ -53,7 +53,7 @@ Key | Example | Note **message**: `Any` | `Collecting payment` | Unserializable JSON values are casted as `str` **timestamp**: `str` | `2021-05-03 10:20:19,650+0200` | Timestamp with milliseconds, by default uses local timezone **service**: `str` | `payment` | Service name defined, by default `service_undefined` -**xray_trace_id**: `str` | `1-5759e988-bd862e3fe1be46a994272793` | When [tracing is enabled](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html), it shows X-Ray Trace ID +**xray_trace_id**: `str` | `1-5759e988-bd862e3fe1be46a994272793` | When [tracing is enabled](https://docs.aws.amazon.com/lambda/latest/dg/services-xray.html){target="_blank"}, it shows X-Ray Trace ID **sampling_rate**: `float` | `0.1` | When enabled, it shows sampling rate in percentage e.g. 10% **exception_name**: `str` | `ValueError` | When `logger.exception` is used and there is an exception **exception**: `str` | `Traceback (most recent call last)..` | When `logger.exception` is used and there is an exception @@ -644,7 +644,7 @@ You might want to continue to use the same date formatting style, or override `l Logger allows you to either change the format or suppress the following keys altogether at the initialization: `location`, `timestamp`, `level`, `xray_trace_id`. === "lambda_handler.py" - > We honour standard [logging library string formats](https://docs.python.org/3/howto/logging.html#displaying-the-date-time-in-messages). + > We honour standard [logging library string formats](https://docs.python.org/3/howto/logging.html#displaying-the-date-time-in-messages){target="_blank"}. ```python hl_lines="7 10" from aws_lambda_powertools import Logger @@ -849,7 +849,7 @@ For **replacing the formatter entirely**, you can subclass `BasePowertoolsFormat #### Bring your own JSON serializer -By default, Logger uses `json.dumps` and `json.loads` as serializer and deserializer respectively. There could be scenarios where you are making use of alternative JSON libraries like [orjson](https://github.com/ijl/orjson). +By default, Logger uses `json.dumps` and `json.loads` as serializer and deserializer respectively. There could be scenarios where you are making use of alternative JSON libraries like [orjson](https://github.com/ijl/orjson){target="_blank"}. As parameters don't always translate well between them, you can pass any callable that receives a `Dict` and return a `str`: @@ -943,7 +943,7 @@ This is a Pytest sample that provides the minimum information necessary for Logg ``` !!! tip - If you're using pytest and are looking to assert plain log messages, do check out the built-in [caplog fixture](https://docs.pytest.org/en/latest/how-to/logging.html). + If you're using pytest and are looking to assert plain log messages, do check out the built-in [caplog fixture](https://docs.pytest.org/en/latest/how-to/logging.html){target="_blank"}. ### Pytest live log feature diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 05c74895eea..354a89305e1 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -35,7 +35,7 @@ def test_alb_event(): def foo(): assert isinstance(app.current_event, ALBEvent) assert app.lambda_context == {} - return 200, TEXT_HTML, "foo" + return Response(200, TEXT_HTML, "foo") # WHEN calling the event handler result = app(load_event("albEvent.json"), {}) @@ -363,7 +363,7 @@ def test_custom_cors_config(): def get_with_cors(): return {} - @app.get("/another-one") + @app.get("/another-one", cors=False) def another_one(): return {} @@ -434,7 +434,7 @@ def foo_cors(): def foo_delete_cors(): ... - @app.post("/foo") + @app.post("/foo", cors=False) def post_no_cors(): ...