Skip to content

Refuse oversized request bodies before buffering them - #10

Draft
ethanzhoucool wants to merge 2 commits into
mainfrom
claude/limit-request-body
Draft

ethanzhoucool wants to merge 2 commits into
mainfrom
claude/limit-request-body

Conversation

@ethanzhoucool

Copy link
Copy Markdown
Owner

Summary

Nothing limited the size of a request body. ExplainRequest's 200 KB code validator only runs after Starlette has buffered and JSON-parsed the whole body, and uvicorn sets no body limit of its own. So a multi-gigabyte POST to any /v1/* endpoint is held in memory in full just to be rejected. On a small single-worker instance that is an easy way to OOM the service.

Separately, DetectRequest had no code limit at all. Measured on main, POST /v1/detect with 10 MB of code returns 200 after 3.1 s of detection work. With the regex fix in #1 that cost is linear; without #1 it is far worse.

Changes

  • api/main.py: a small pure-ASGI BodySizeLimit middleware with a limit of MAX_BODY_BYTES = 10 * MAX_SOURCE_BYTES (2 MB).
    • If the request declares a Content-Length over the limit, it gets 413 {"error": "payload_too_large", ...} before anything is read.
    • A chunked body with no Content-Length is counted as it arrives. Once it passes the limit the middleware raises HTTPException(413) from receive, while the endpoint is reading its body, and FastAPI turns that into a normal 413 response.
    • Why 10×: JSON escaping can turn one byte of source into six (\u0001), so the largest legitimate 200 KB snippet can approach 1.2 MB. A test pins that case.
  • api/schemas.py: DetectRequest.code gets the same size validator as ExplainRequest, factored into one helper. Oversized detect requests now return 422, which is what /v1/explain already returns. /v1/analyze already returned 413 via SourceTooLarge and is unchanged.

Testing

  • pytest on Python 3.13, 3.12 and 3.11: 151 passed, 3 skipped. ruff is clean, and mypy reports the same 26 errors as before.
  • New tests in tests/test_api.py. The first three fail on main:
    • test_oversized_body_is_refused_before_it_is_read: a 3 MB body with Content-Length gets 413.
    • test_oversized_chunked_body_is_refused: a 3 MB generator body (chunked) gets 413.
    • test_detect_enforces_the_source_limit: 360 KB to /v1/detect gets 422.
    • test_largest_legitimate_snippet_still_fits_the_body_limit: 199,999 control characters (a 1.19 MB JSON body) still get 200.
  • I also ran a real uvicorn locally and sent it a 5 MB body with curl: 413 with Content-Length, 413 with Transfer-Encoding: chunked, and 200 for a normal request. The server was stopped afterwards.

Notes

The in-process rate limiter keys on request.client.host. Behind Render's proxy that may be the proxy's address rather than the user's, and then all visitors share one 60-requests-per-minute bucket. I couldn't verify Render's networking from here, so I left it alone. If it applies, uvicorn's --forwarded-allow-ips (or reading X-Forwarded-For) would fix it.


Generated by Claude Code

Nothing capped the size of a request body. The `code` validators only
run after Starlette has read and JSON-parsed the whole body, so a
multi-gigabyte POST was held in memory in full just to be rejected,
which is enough to take down a small single-worker instance.
/v1/detect had no size check at all and ran detection on whatever it
got: 10MB took 3s.

A small ASGI middleware now returns 413 for a declared Content-Length
over 2MB (ten times the 200KB source limit, which leaves room for
worst-case JSON escaping) and counts chunked bodies as they arrive.
DetectRequest gets the same `code` limit as ExplainRequest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UigC7feaY8NkAy9oay3GDW
Annotating with `Any` instead of `starlette.types` keeps this change
off the import block that the SSE threadpool change also edits, so the
two can merge in either order without a conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UigC7feaY8NkAy9oay3GDW

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants