Skip to content

Bug: OpenAPI validation rejects or truncates query and header values that contain commas #8508

Description

@chancesong10

Expected Behaviour

With enable_validation=True, a scalar query string or header parameter should receive its value unchanged, including any commas. For example, ?search=hello,world into search: str should give "hello,world", and X-Forwarded-For: 203.0.113.9, 10.0.0.1 into a str header should give the whole chain.

Splitting on commas should only apply to sequence parameters such as List[str], which is what it's there for.

Current Behaviour

resolved_query_string_parameters (and resolved_headers_field for HTTP API / Lambda Function URL events) splits every value on commas. After that, _process_scalar_param only unwraps lists with one item, so a scalar parameter whose value contains a comma gets a list:

  • A plain str query or header parameter fails validation with 422 string_type.
  • A str field on a Pydantic model used with Query() gets only the part before the first comma, and the handler runs with truncated data. _normalize_field_value takes value[0].

I can reproduce this with APIGatewayHttpResolver, LambdaFunctionUrlResolver, ALBResolver (single-value mode, where ALB never joins values with commas) and VPCLatticeV2Resolver. It breaks ordinary inputs like search text, ?coords=49.26,-123.25, ?sort=name,asc, and X-Forwarded-For on any request that went through a proxy.

This is the same problem as #6520, which was fixed for BedrockAgentResolver only (#6777).

Code snippet

from typing import Annotated

from pydantic import BaseModel

from aws_lambda_powertools.event_handler import APIGatewayHttpResolver
from aws_lambda_powertools.event_handler.openapi.params import Header, Query

app = APIGatewayHttpResolver(enable_validation=True)


class SearchParams(BaseModel):
    search: str
    tags: list[str]


@app.get("/scalar")
def scalar(search: str):
    return {"search": search}


@app.get("/header")
def header(x_forwarded_for: Annotated[str, Header()]):
    return {"x_forwarded_for": x_forwarded_for}


@app.get("/model")
def model(params: Annotated[SearchParams, Query()]):
    return params.model_dump()


def event(path, query=None, headers=None):
    return {
        "version": "2.0",
        "routeKey": f"GET {path}",
        "rawPath": path,
        "rawQueryString": "",
        "headers": headers or {},
        "queryStringParameters": query,
        "requestContext": {
            "http": {"method": "GET", "path": path, "protocol": "HTTP/1.1", "sourceIp": "203.0.113.9", "userAgent": "x"},
            "stage": "$default",
            "requestId": "id",
        },
        "isBase64Encoded": False,
    }


for e in [
    event("/scalar", query={"search": "hello,world"}),
    event("/header", headers={"x-forwarded-for": "203.0.113.9, 10.0.0.1"}),
    event("/model", query={"search": "hello,world", "tags": "a,b"}),
]:
    result = app.resolve(e, {})
    print(int(result["statusCode"]), result["body"])

Output:

422 {"statusCode":422,"detail":[{"loc":["query","search"],"type":"string_type"}]}
422 {"statusCode":422,"detail":[{"loc":["header","x-forwarded-for"],"type":"string_type"}]}
200 {"search":"hello","tags":["a","b"]}

Possible Solution

Join the split values back together for non-sequence parameters, in _process_scalar_param and in _process_model_param for non-sequence model fields. Sequence parameters would still be split as they are today.

Values are split with a plain str.split(","), so joining them with "," gives back exactly what the event contained, spaces included. It also keeps working with ALB's decode_query_parameters, which decodes after splitting.

I have this change with tests ready and would like to open the PR.

Steps to Reproduce

  1. Create any HTTP resolver with enable_validation=True.
  2. Add a route with a str query or header parameter, or a Pydantic model with a str field used with Query().
  3. Send a request where that value contains a comma, e.g. ?search=hello,world.
  4. The request is rejected with 422, or the model field only contains hello.

Powertools for AWS Lambda (Python) version

latest (3.35.0)

AWS Lambda function runtime

3.10

Packaging format used

PyPi

Debugging logs

N/A: reproduced locally with the snippet above.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions