Our security gate was meant to protect a small mapped subset of routes. The middleware shape we used made the risk larger than that.
The gate entered FastAPI as a Starlette (request, call_next) function mounted with app.middleware("http"). That mount wraps the whole application in Starlette's BaseHTTPMiddleware. The important behaviour is not the function signature itself. It is what the layer does with responses: it buffers the downstream response and reconstructs it before sending.
In the live application, that reconstruction could retain an inner Content-Length header while emitting a different number of body bytes. Uvicorn then raised:
RuntimeError: Response content longer than Content-Length
The exception did not surface on a route the gate protected. It surfaced on ordinary GET traffic to an internal status endpoint that the gate does not protect and never evaluates. On 2026-09-02, ten ordinary GET requests to that endpoint produced four new exceptions in the service's error log. The design spec records that as current production behaviour, not historical residue.
That is the footgun: a function-style middleware wraps every request so it can inspect the few routes it cares about. A defect in that wrapper is therefore an application-wide defect. It is not confined to protected routes, and it is not necessarily exercised by the security decision at all.
A second service had the same wrapper shape around media and streaming responses, the workload where this layer is most commonly known to misbehave, but its own log did not show this exception. That distinction matters. We had one live symptom, one same-wrapper preventive migration, and five more latent consumers later migrated for the same shape. We are not claiming that both initial consumers produced the production failure.
The Fix Was Not Another Wrapper Patch
The tempting fixes all stayed too close to the broken shape.
One alternative was a route-specific shim: evaluate only the handful of protected routes locally, inside each consumer, instead of wrapping the whole application. We rejected that because it would duplicate the security boundary outside the library that owns it. Each consumer would need to re-implement route and template matching. It would also leave the second service running the same unsafe wrapper shape. Worse, a predicate bug in a hand-rolled shim could silently bypass a protected route once enforcement was later turned on, which is exactly the kind of failure that stays invisible until it matters.
The other alternative was to strip or recalculate Content-Length on the re-emitted response. That would treat the visible symptom after the unnecessary response transformation had already happened. It would keep the buffering and re-emission layer in place. It also risked weakening correct file-response metadata that other code depends on.
The fix was a purpose-built pure-ASGI middleware for the gate. It replaced the function-style wrapper at the mount point in each consumer and passed ASGI send events through untouched. It never buffered the downstream body. It never re-emitted a reconstructed body. The change was a middleware-shape change, not a change in security semantics.
The adapter preserved the existing decision contract exactly: off and dark modes pass through; shadow mode audits without blocking; enforce mode denies with the same status code and JSON body. It preserved the existing fail-open behaviour when gate evaluation itself failed unexpectedly before downstream dispatch. It also had to guarantee that downstream application code was dispatched at most once.
That last requirement is a useful forcing function. When a middleware is security-sensitive, "works on the observed route" is not a sufficient repair. The adapter has to preserve the gate's policy contract while removing the transport-layer behaviour that made unrelated responses fragile.
The Method Was Part Of The Repair
This did not land as an isolated patch over a production symptom. Implementation started from two worktrees proven clean against their base commits. An independent review returned three findings before merge, each reproduced as a failing test and closed under TDD, and the follow-up review returned none. The library and consumer suites passed at the pre-merge gate, again after the operator-approved restart of the first two consumers, and again after the rollout to the remaining five, with the count of Content-Length exceptions in the live error log holding flat throughout: zero new occurrences post-fix. Nothing was merged or deployed on the implementer's own judgement; code review and operator approval were required before anything touched a running service.
That process is not decoration around the work. It is how the repair avoided turning a middleware bug into a security semantics change, or a local incident into a quiet drift across consumers.
Where It Recurred
Seven middleware mounts across five repositories carried the same wrapper shape: the one service with the live symptom, a second with the same shape and no occurrence in its log, and five latent consumers found by looking for the shape rather than the symptom. All seven are migrated and live.
The useful conclusion stays narrow. Starlette's BaseHTTPMiddleware made the gate's wrapper an application-wide response transformer. Our production symptom appeared on ordinary, unprotected traffic. The repair was to stop transforming downstream bodies at all.
