Nobody has started this yet — be first.
Business impact
An upload endpoint with no size cap is a resource-exhaustion vector -- intentional (a hostile client deliberately sending a huge body to exhaust gateway or upstream memory/disk) or accidental (a buggy client retry-looping a multi-GB file). This gateway already proved, at some effort, that it can stream a 700MB response through without buffering it; a size limit implemented by buffering the request body would throw away exactly that property on the other side of the same connection.
Problem
RouteConfig.max_request_body_bytes exists as a field (default None = unlimited) so it can be set in routes.yaml or via the admin API, but nothing reads it. A client can stream an arbitrarily large request body into any route and the gateway will happily forward every byte upstream, no matter what.
Current behavior
A route configured with max_request_body_bytes still forwards a request body of any size unchanged -- send_upstream_request in app/proxy/forwarder.py passes request.stream() straight through as content=, and nothing anywhere counts bytes against the configured limit.
Expected behavior
When a route sets max_request_body_bytes, a request whose body exceeds that many bytes is rejected with 413 Payload Too Large -- without ever buffering the body to measure it.
Steps to reproduce
Why this matters
request.stream() is an async generator that Starlette drains lazily, one chunk at a time, straight from the client socket. A size limit has to be enforced by counting bytes as they pass through that generator -- raising as soon as the running total crosses the limit -- not by materializing the whole body first and checking its length afterward, which would defeat the gateway's core streaming guarantee for every request on a size-limited route.
Suggested approach
send_upstream_request currently passes request.stream() directly as content=. Consider wrapping it in your own async generator that increments a running total per chunk and raises once the total exceeds route.max_request_body_bytes, then yields chunks through unchanged until then -- the wrapper is what actually gets passed to httpx instead of the raw stream. Look at how / are defined and caught in for the pattern to follow when turning your new exception into a 413 response.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_11_max_request_body_size.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b feat/max-request-body-sizeFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/max-request-body-sizeSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inUpstreamTimeoutErrorUpstreamConnectErrorapp/routers/proxy.pyhttp://localhost:8080fast_echo/echo/.../large/{size_mb}slow_upstream/delay/{s}/flaky/always-downroutes.yamlcurl localhost:8080/healthzcurl localhost:8080/readyz/admin/*Authorization: Bearer <GATEWAY_ADMIN_TOKEN>You do not need Docker running at all to work the tickets — the test suite fakes Redis with fakeredis (plus lupa for real Lua-script support), so everything runs against a plain virtualenv:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/pytest practice_tickets/tests/test_ticket_09_breaker_threshold_off_by_one.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
./practice_tickets/run_tickets.sh 06 09 15 # just a few, by number
All 15 tests fail at the start; as you fix each ticket in app/, its test (and only its test) should flip to passing. .venv/bin/pytest tests/ runs the project's own main suite — as of writing it shows 8 pre-existing failures caused by ticket bugs 02, 04, 06, and 09; fixing those tickets fixes those failures too, not a separate task.
Level 2
Implement a feature
Extend the system within its own patterns.