Skip to content

Forms and uploads

Request.form_data() parses URL-encoded and multipart bodies into the same ordered FormData / File surface on ASGI, Cloudflare Python Workers, direct tests, and AWS Lambda.

Bounded parsing

The zero-argument call uses conservative defaults:

Limit Default
Complete form body 32 MiB
One file 32 MiB
One text field 1 MiB
Parts 1,000
Headers per part 16 KiB
Native in-memory file threshold 1 MiB

Set an application-specific contract explicitly:

from hayate import (
    File,
    FormDataError,
    FormDataLimitError,
    FormDataLimits,
    HTTPException,
)
from hayate.middleware import body_limit

upload_limits = FormDataLimits(
    max_body_bytes=12 * 1024 * 1024,
    max_file_bytes=10 * 1024 * 1024,
    max_field_bytes=64 * 1024,
    max_parts=20,
    max_header_bytes=8 * 1024,
    file_memory_bytes=512 * 1024,
)


@app.post("/documents", body_limit(max_size=12 * 1024 * 1024))
async def documents(c):
    try:
        form = await c.req.form_data(upload_limits)
    except FormDataLimitError as exc:
        raise HTTPException(413, title="Payload Too Large", detail=str(exc)) from exc
    except FormDataError as exc:
        raise HTTPException(400, title="Invalid form", detail=str(exc)) from exc

    async with form:
        upload = form.get("file")
        if not isinstance(upload, File):
            raise HTTPException(400, title="A file is required")

        async for chunk in upload.stream():
            await destination.write(chunk)

    return c.json({"stored": True}, 201)

body_limit() is the coarse transport guard. FormDataLimits is a second parser-level boundary that also limits part counts, individual fields, and part headers.

Native spooling and Workers

On native Python, a streamed multipart file stays in memory until file_memory_bytes and then moves to an unnamed temporary file. File.stream() reads that file in bounded chunks; File.bytes() and File.text() remain available for compatible existing code.

On Workers/Pyodide, Hayate never claims filesystem spooling. The same parser stays extension-free and rejects input at the configured limits. Choose a Workers limit that fits the platform and stream accepted content to R2 or another application-owned destination as early as the deployment permits.

Use async with form or await form.close() to release native temporary files deterministically. Parser failures and exceeded limits close partial files automatically.

Validation

The core validator("form", ...) converts malformed forms to RFC 9457 400 responses and configured limit failures to 413. It keeps uploaded files open through the downstream handler and closes them on success or failure when the middleware unwinds. hayate-openapi typed form parameters use the same FormData and File values; multipart schemas do not create a second upload type.