Fix unbounded memory growth in Conditional middleware#214
Conversation
The Conditional middleware's Response transform taps the per-connection `early-responses` Supplier but never completes it. The RequestResponse middleware completes the same Supplier via `LAST ... .done` when its response pipeline ends; Conditional omitted this, so the Supplier (and the connection-scoped state reachable from its subscription) was retained, leaking memory on every request that passes through any Conditional middleware (e.g. Cro::APIToken-based auth, Cro's own auth middleware). Reproduction: a server with a no-op Conditional middleware grows RSS linearly and unboundedly under repeated requests (~80 KB/request), while an equivalent RequestResponse middleware plateaus. Adding the matching `.done` brings Conditional in line with RequestResponse and the growth stops. All existing tests pass (t/http-middleware, t/http-router, t/router-auth, t/http-auth-*, t/http-session-*).
|
Worth flagging the blast radius: this isn't limited to apps that explicitly write a Direct demo, a no-op
Same one-line fix resolves it. |
|
Thanks for the PR. This looks good to me. Since it’s quite a deep fix, I’d like to see a second review. |
|
Great to see this PR! @zaucker could you create a PR for the other fix as well? |
Stock Cro from 'zef install --deps-only .' has an unbounded-memory-growth bug (Cro::HTTP::Middleware::Conditional) and, on Cro::HTTP::Router 0.8.12+, an OpenAPI-include crash. Document the patched fork branches to install until croservices/cro-http#214 and cro-openapi-routes-from-definition#15 are released. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Which other fix? This one? |
|
Thanks for explaining the background. It is very helpful to see your experience with Cro! I have pulled your PR and tested with Air and raku.org on moar-2026.05 - all functional tests are good. Looking at croservices/cro-openapi-routes-from-definition#15 I would say we can handle as two parallel PRs ... please note my comment over there. I just want to spelunk the code to eyeball what OK - I am happy with the change. Will merge now and do a point release of (We can do a point release of |
See my answer there :-)
Cool, thanks for the fast handling, makes my life easier without a locally patched module.
Sounds good. |


Problem
Any HTTP server with a
Cro::HTTP::Middleware::Conditionalin its pipeline leaks memory: RSS grows linearly and without bound as requests are served. This affects, among others,Cro::APIToken-based authentication (its middlewaredoes Cro::HTTP::Middleware::Conditional) and Cro's own auth middleware — i.e. every authenticated request leaks.Root cause
ConditionalandRequestResponseare built the same way: aRequestand aResponsetransform that share a per-connectionSkipPipelineStateholding aSupplier $.early-responses. TheResponsetransform taps that Supplier withwhenever.RequestResponse'sResponsecompletes the Supplier when its pipeline ends:Conditional'sResponsenever did. Theearly-responsesSupplier is therefore never completed, so the subscription (and the connection-scoped state reachable from it) is retained — leaking per request.Fix
One line — mirror
RequestResponseand.donethe Supplier when the pipeline ends:Reproduction & verification
Minimal server with a no-op
Conditionalmiddleware, hammered with trivial GET requests:ConditionalmiddlewareRequestResponsemiddleware (control)Verified end-to-end in a real app (Cro::APIToken auth): a
/api/v1endpoint that previously grew ~0.27 MB/request now plateaus.Tests
All existing tests pass against the patched source:
t/http-middleware(24),t/http-router(439),t/router-auth,t/http-auth-basic,t/http-auth-webtoken-bearer,t/http-session-inmemory.I'm happy to add a regression test if you'd like, though a memory-growth assertion is inevitably timing/threshold-based — guidance welcome on the form you'd prefer.