A FastAPI reverse proxy that streams every request and response without ever buffering a body in full, with hot-reloadable routing, per-route auth, Redis token-bucket rate limiting, and a per-upstream circuit breaker.
Nobody has started this yet — be first.
/readyz reports 200 even when Redis is unreachable
`/readyz` is what a load balancer or Kubernetes uses to decide whether to send traffic to this gateway instance. Every rate-limited route depends on Redis being reachable -- if Redis is down, rate limiting either silently fails open or every request 500s, depending on the route. Right now `/readyz` returns HTTP 200 even when its own Redis ping fails, so nothing upstream of the gateway (load balancer, k8s readiness probe, an on-call dashboard) can tell the difference between "fine" and "broken." A Redis outage becomes an outage that traffic keeps getting routed into, discovered only when customers start complaining -- not a clean, automatic failover.
About this project
A production-style FastAPI dynamic reverse proxy / API gateway. It sits in front of one or more upstream services, decides where to send a request based on a hot-reloadable route table (flat list, longest-path-prefix wins), and applies auth, rate limiting, header/path transformation, and circuit-breaking per route — all without ever materializing a request or response body as a single object, regardless of size: the request body forwarded upstream is , never , and the response is opened with and streamed back chunk by chunk as yields, so proxying a 1KB response and a 700MB one cost the gateway the same, small, constant amount of memory. Every proxied request goes through the same ordered pipeline — match route, check method, authenticate (pluggable per route: none / api_key / bearer), rate limit (Redis token bucket via one atomic Lua , keyed by route + client), acquire the per-upstream circuit breaker, forward, then record the outcome — and each stage can reject the request before any more expensive stage runs. The route table is reloadable from disk ( + ) or live via a full admin CRUD API (/, plus to write the in-memory table back to disk), none of it requiring a restart.
Bearer-auth routes reject every legitimate request
Any route configured with `auth.type: bearer` is completely unusable -- every single request gets 401'd, including ones with a perfectly valid token. If a customer-facing API is fronted by a bearer-auth route, this is a full outage for that route, not a degradation: 100% of traffic fails, and the error message ("missing x-api-key header") actively misleads whoever is debugging it, since the client is correctly sending `Authorization: Bearer ...`, not `x-api-key`.
bytesrequest.stream()await request.body()client.send(request, stream=True)response.aiter_raw()EVALroutes.yamlPOST /admin/routes/reloadPUTDELETE /admin/routes/{id}persistFifteen practice tickets are injected into an otherwise-working codebase: nine are real, minimal, single-mistake bugs (an inverted boolean, a swapped comparison operator, a wrong-order pair of path transforms, a silently-broadened except, a division never guarded against its one degenerate input, a check-then-act pair separated by a genuine await that reintroduces a race a lock was supposed to prevent), and six are half-built enhancements — a schema field with no code reading it yet, a function that always returns one hardcoded value, an endpoint that always 501s, a metric that is defined but never .set() — the smallest thing that compiles, never a disguised version of the real fix. Ordered easy to hard, each ticket has its own currently-failing pytest test that goes green when (and only when) that ticket is fixed; no solutions are given anywhere in the ticket docs. A few of the injected bugs are real enough that they also break tests in the project's own main suite (eight of them, as of writing) — expected, not a second problem to chase separately, since fixing the ticket fixes both. The hardest ticket, TICKET-15, is a genuine, reproducible asyncio concurrency race in the circuit breaker's half-open probe admission, verified by deliberately restoring the atomic fix and confirming the test goes green before reverting it back to the buggy version.
.set()asynciohttp://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.
How to run it locally, step by step.