aws-powertools / powertools-lambda-python

A developer toolkit to implement Serverless best practices and increase developer velocity.
https://docs.powertools.aws.dev/lambda/python/latest/
MIT No Attribution
2.81k stars 390 forks source link

Bug: CORS headers not appending to API Gateway REST API responses #3849

Closed pavitra-infocusp closed 6 months ago

pavitra-infocusp commented 6 months ago

Expected Behaviour

The responses should return with CORS headers set.

Current Behaviour

The CORS headers are missing, unless, explicitly set using Response class.

Code snippet

import json

from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response, CORSConfig
from aws_lambda_powertools.utilities.typing import LambdaContext

cors_config = CORSConfig()
app = APIGatewayRestResolver(cors=cors_config)

@app.get('/stats')
def stats():
    return dict(data=dict(updates=99))

@app.get("/test")
def my_demo_resource():
    res = dict(data="value")

    return Response(
        status_code=200,
        content_type="application/json",
        headers={
            "Cookies": ""
        },
        body=json.dumps(res),
    )

def lambda_handler(event: dict, context: LambdaContext) -> dict:
    # event['headers'] = {'Origin': '*'} # inject Origin header for CORS to work
    return app.resolve(event, context)

print(lambda_handler({"path": "/stats", "httpMethod": "GET"}, None))
print(lambda_handler({"path": "/test", "httpMethod": "GET"}, None))

Possible Solution

Inject the Origin header to the event param before app.resolve() call.

Steps to Reproduce

Uncomment the line after lambda_handler function declaration, and run the code locally.

Powertools for AWS Lambda (Python) version

latest

AWS Lambda function runtime

3.12

Packaging format used

Lambda Layers

Debugging logs

Current behaviour:

{'statusCode': <HTTPStatus.OK: 200>, 'body': '{"data":{"updates":99}}', 'isBase64Encoded': False, 'multiValueHeaders': defaultdict(<class 'list'>, {'Content-Type': ['application/json']})}
{'statusCode': 200, 'body': '{"data": "value"}', 'isBase64Encoded': False, 'multiValueHeaders': defaultdict(<class 'list'>, {'Cookies': [''], 'Content-Type': ['application/json']})}

Expected behaviour:

{'statusCode': <HTTPStatus.OK: 200>, 'body': '{"data":{"updates":99}}', 'isBase64Encoded': False, 'multiValueHeaders': defaultdict(<class 'list'>, {'Content-Type': ['application/json'], 'Access-Control-Allow-Origin': ['*'], 'Access-Control-Allow-Headers': ['Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key']})}
{'statusCode': 200, 'body': '{"data": "value"}', 'isBase64Encoded': False, 'multiValueHeaders': defaultdict(<class 'list'>, {'Cookies': [''], 'Content-Type': ['application/json'], 'Access-Control-Allow-Origin': ['*'], 'Access-Control-Allow-Headers': ['Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key']})}
boring-cyborg[bot] commented 6 months ago

Thanks for opening your first issue here! We'll come back to you as soon as we can. In the meantime, check out the #python channel on our Powertools for AWS Lambda Discord: Invite link

heitorlessa commented 6 months ago

Thank you for the report @pavitra-infocusp - looking into this shortly today.

heitorlessa commented 6 months ago

I might be missing something but I can't reproduce - here's an endpoint I've created to test (code at the bottom): https://pm5mz0wx61.execute-api.eu-west-1.amazonaws.com/Prod/todos

Questions

is this also happening on pre-flight operations OPTIONS? Could you share how you setup API Gateway?

I've used both cURL and a browser JS test.

curl -s -I -H "Origin: https://docs.powertools.aws.dev" -X GET "https://pm5mz0wx61.execute-api.eu-west-1.amazonaws.com/Prod/todos"


Infra Cors SETUP

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
    Complete infra to reproduce lack of CORS headers in #3849

Globals:
    Function:
        Timeout: 60
        MemorySize: 512
        Tracing: Active
        Runtime: python3.11
        Architectures:
            - x86_64
        Environment:
            Variables:
                POWERTOOLS_METRICS_NAMESPACE: TodoApp
                LOG_LEVEL: INFO
                TZ: "Europe/Amsterdam"
    Api:
        TracingEnabled: true
        Cors: # see CORS section: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#cors
            AllowOrigin: "'*'"
            AllowHeaders: "'Content-Type,Authorization,X-Amz-Date'"
            MaxAge: "'5'"

Resources:
    #####################################################
    # API Todos
    #
    # Flow: API Gateway -> Lambda
    #           |
    #           v
    #        /todos -> Lambda
    #
    #####################################################

    GetTodosFunction:
        Type: AWS::Serverless::Function # More info about Function Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html
        Properties:
            Handler: app.lambda_handler
            CodeUri: todo_api
            Events:
                HelloPath:
                    Type: Api
                    Properties:
                        Path: /todos
                        Method: GET
                TodoById:
                    Type: Api
                    Properties:
                        Path: /todos/{todo_id}
                        Method: GET
                CreateTodo:
                    Type: Api
                    Properties:
                        Path: /todos
                        Method: POST
                SwaggerUI:
                    Type: Api
                    Properties:
                        Path: /swagger
                        Method: GET
                SwaggerUICSS:
                    Type: Api
                    Properties:
                        Path: /swagger.css
                        Method: GET
                SwaggerUIJS:
                    Type: Api
                    Properties:
                        Path: /swagger.js
                        Method: GET
                # Powertools env vars: https://awslabs.github.io/aws-lambda-powertools-python/#environment-variables
            Environment:
                Variables:
                    POWERTOOLS_SERVICE_NAME: TodoAPI
                    # POWERTOOLS_DEV: true

Outputs:
    GetTodosFunction:
        Description: "Hello World Lambda Function ARN"
        Value: !GetAtt GetTodosFunction.Arn

    TodoEndpoint:
        Description: "API Gateway endpoint URL for Prod environment for Todos"
        Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"

    GetTodosAPI:
        Description: "API Gateway endpoint URL to fetch todos"
        Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/todos"

Code used

from typing import Annotated, Optional

import requests
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler import (
    APIGatewayRestResolver,
    CORSConfig,
)

from aws_lambda_powertools.event_handler.openapi.params import Query
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext
from aws_lambda_powertools.utilities.parser import Field, BaseModel

tracer = Tracer()
logger = Logger()
cors = CORSConfig(max_age=2)  # short enough to avoid caching issue
app = APIGatewayRestResolver(enable_validation=True, cors=cors)
app.enable_swagger(path="/swagger")

class Todo(BaseModel):
    userId: int
    id_: Optional[int] = Field(alias="id", default=None)
    title: str
    completed: bool

@app.get("/todos")
@tracer.capture_method
def get_todos(
    completed: Annotated[str | None, Query(min_length=4)] = None
) -> list[Todo]:
    url = "https://jsonplaceholder.typicode.com/todos"

    if completed is not None:
        url = f"{url}/?completed={completed}"

    todo = requests.get(url)
    todo.raise_for_status()

    return todo.json()

@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
@tracer.capture_lambda_handler
def lambda_handler(event: dict, context: LambdaContext) -> dict:
    ret = app.resolve(event, context)

    # Logging response to be triple sure Event Handler is appending CORS headers
    logger.info("Returning response", response=ret)
    return ret
pavitra-infocusp commented 6 months ago

Actually, it's not an issue with Powertools at all. It failed on the preflight requests stating CORS issue. Since I'm building a Chrome extension, Chrome doesn't set the Origin header with the request, unless the URL is specified in the host_permissions in the manifest. Sorry for the misunderstanding.

github-actions[bot] commented 6 months ago

⚠️COMMENT VISIBILITY WARNING⚠️

This issue is now closed. Please be mindful that future comments are hard for our team to see.

If you need more assistance, please either tag a team member or open a new issue that references this one.

If you wish to keep having a conversation with other community members under this issue feel free to do so.

heitorlessa commented 6 months ago

I'm glad you found out @pavitra-infocusp -- better safe than sorry :) no worries from our side. Have an awesome day ahead with your new extension!

pavitra-infocusp commented 6 months ago

Thanks for your support!