powertools-lambda-python: Bug: APIGatewayHttpResolver with validation enabled returns HTTP 422 when using single value query parameters

Expected Behaviour

When using Amazon Api Gateway REST API to invoke an AWS Lambda using proxy integration, calling API with a single query parameter should return HTTP 200 with Lambda Powertools validation enabled.

This is also how it is documented here: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#validating-query-strings

Current Behaviour

Calling API with GET /my-path?productType=Category1 returns HTTP 422:

{
    "statusCode": 422,
    "detail": [{
        "loc": ["query", "productType"], 
        "type": "type_error.str"
    }]
}

Code snippet

Bug can be reproduced with this code inside a Lambda function:


app = api_gateway.APIGatewayRestResolver(
    enable_validation=True
)

@app.get("/my-path")
def my_handler(
    product_type: Annotated[str | None, Query(alias="productType")] = None,
) -> api_gateway.Response[api_model.GetAvailableProductsResponse]:
    ...

Possible Solution

Can be fixed by changing query param type to list:

@app.get("/my-path")
def my_handler(
    product_type: Annotated[list[str] | None, Query(alias="productType")] = None,
) -> api_gateway.Response[api_model.GetAvailableProductsResponse]:
    """Lists available products"""
    ...

Steps to Reproduce

In AWS:

  1. Create a REST API in the Amazon Api Gateway Service.
  2. Create a Lambda function with Lambda Powertools configured as in the code examples.
  3. Configure the API Gateway with proxy integration to the Lambda function.
  4. Invoke the API with a query parameter.

Locally:

  1. Create unit tests for a Lambda function as described here: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#testing-your-code
  2. Run the test using REST API proxy integration payload. This payload always contains 2 attributes for the query string parameters, regardless if it’s single value or multi value in REST API proxy integration:
    {
        ...
        "queryStringParameters": { "productType": "Category1" },
        "multiValueQueryStringParameters": { "productType": ["Category1"] }
    }
    

The issue is this line of code: https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py#L123

Lambda Powertools should additionally check dictionary values in multiValueQueryStringParameters if they contain only one value, and validate accordingly to be of type str instead of list[str].

Powertools for AWS Lambda (Python) version

2.33.0

AWS Lambda function runtime

3.12

Packaging format used

PyPi

Debugging logs

No response

About this issue

  • Original URL
  • State: closed
  • Created 5 months ago
  • Reactions: 3
  • Comments: 15 (11 by maintainers)

Most upvoted comments

Coming up in this week’s release! Just merged Ruben’s PR

Got it, was able to reproduce the problem! Fix incoming

No that helps, let me investigate today and I’ll get back to you today!

The issue isn’t related to the alias but the fact that the openapi_validation.py handles all query strings with _normalize_multi_query_string_with_param and takes the first value of the item.

With "queryStringParameters": { "productType": "Category1" } it’s normalize to: "productType": "C"

With "multiValueQueryStringParameters": { "productType": ["Category1"] } it’s normalize correctly to: "productType": "Category1"

I was able to find this as I was trying to validate the min length of a query string parameter (def get_hello(fname: Annotated[Optional[str], Query(min_length=4)] = None) -> HelloWorld: and fname was always too short… as it was always truncated to 1 character.

The line truncating the value is here

@DariusKunce Confirmed! We’ll add this into the next sprint to fix

def test_weird_issue():
    # GIVEN a Http API V2 proxy type event
    app = APIGatewayRestResolver(enable_validation=True)

    class FunkyTown(BaseModel):
        parameter: str

    @app.get("/my/path")
    def my_path(
        parameter: Annotated[str | None, Query(alias="parameter1")] = None,
    ) -> Response[FunkyTown]:
        assert isinstance(app.current_event, APIGatewayProxyEvent)

        assert parameter == "value"
        return Response(200, content_types.APPLICATION_JSON, FunkyTown(parameter=parameter))

    result = app(load_event("apiGatewayProxyEvent.json"), {})

    print(result)
    assert result["statusCode"] == 200