Skip to content

Bug: body validation crashes on non-ASCII multipart data and accepts a JSON array for an object parameter #8578

Open

Description

@leandrodamascena

Expected Behaviour

With enable_validation=True:

  • a multipart/form-data body with non-ASCII text, or without a boundary, returns 422/415;
  • a JSON array sent to a parameter declared as an object returns 422;
  • a Content-Disposition header with filename= before name= is parsed.

Current Behaviour

Three body validation problems in middlewares/openapi_validation.py, all confirmed through a real API Gateway REST API:

  1. Non-ASCII multipart crashes (502). A text part containing café raises UnicodeDecodeError, which escapes because except ValueError: raise re-raises Unicode errors (they subclass ValueError). Non-base64 bodies are encoded with latin-1, which is lossy, and then decoded as utf-8. A missing boundary raises a bare ValueError. Lines 221-241, 761, 785.
  2. JSON array accepted for an object parameter. [{"name":"a","price":1},{"name":"b","price":2}] sent to def create(item: Item) returns 200 with the first element only. The list unwrapping in _normalize_field_value (line 541) exists for parse_qs form values but is applied to JSON bodies too. Same with Annotated[str, Body()] and ["a","b"].
  3. filename= before name= misparsed. find('name="') matches inside filename="…" (line 800), so the part is not found and the request gets 422 missing.

Code snippet

from typing import Annotated
from pydantic import BaseModel
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.event_handler.openapi.params import File, Form
from tests.functional.utils import load_event

def v1(method, path, body, ct):
    e = load_event("apiGatewayProxyEvent.json")
    e["httpMethod"] = method; e["path"] = path; e["body"] = body; e["isBase64Encoded"] = False
    e["headers"] = {"content-type": ct}; e["multiValueHeaders"] = {"content-type": [ct]}
    return e

app = APIGatewayRestResolver(enable_validation=True)

@app.post("/up")
def up(file: Annotated[bytes, File()], note: Annotated[str, Form()]):
    return {"note": note}

class Item(BaseModel):
    name: str
    price: float

@app.post("/items")
def create(item: Item):
    return item.model_dump()

multipart = ('--XX\r\nContent-Disposition: form-data; name="file"; filename="a.txt"\r\n\r\nhello\r\n'
             '--XX\r\nContent-Disposition: form-data; name="note"\r\n\r\ncafé\r\n--XX--\r\n')
app.resolve(v1("POST", "/up", multipart, "multipart/form-data; boundary=XX"), None)
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9

r = app.resolve(v1("POST", "/items", '[{"name":"a","price":1},{"name":"b","price":2}]', "application/json"), None)
print(r["statusCode"], r["body"])  # 200 {"name":"a","price":1.0}  (expected 422)

Possible Solution

Encode non-base64 bodies as utf-8; decode text parts inside the generic except Exception and raise RequestValidationError; turn the missing-boundary case into RequestUnsupportedContentType. Only unwrap lists for form/multipart-sourced values. Parse Content-Disposition with a proper parameter parser or match (^|;\s*)name=.

Steps to Reproduce

Run the snippet above with the tests/events fixtures. Full context in #8576.

Powertools for AWS Lambda (Python) version

latest

AWS Lambda function runtime

3.13

Packaging format used

PyPi

Debugging logs

Warning

This issue is ready to be picked up, by humans or by agents. If you want it, comment here first so we can assign it to you, and please take only one or two issues at a time so others can join in too. Thanks!

Activity

  1. added this to the 3.38.0 milestone on Oct 18, 2026
  2. MTirnova commented on Oct 18, 2026

    @MTirnova

    Hi! I'd like to pick this one up (I also have #8620 open for #8587, so this would be my second). Plan:

    1. Multipart: decode text parts as UTF-8 without the lossy latin-1 round trip, and turn UnicodeDecodeError / a missing boundary into a proper 422/415 instead of letting them escape as a 502.
    2. Only apply the single-element list unwrapping in _normalize_field_value to form/query values, not to JSON bodies, so a JSON array sent to an object parameter returns 422.
    3. Parse Content-Disposition parameters properly so filename= before name= works.

    I'll add functional tests for each case.

  3. leandrodamascena commented on Oct 18, 2026

    @leandrodamascena
    ContributorAuthor

    Hey @MTirnova pls go ahead.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions