|
| 1 | +# modern-di-aiohttp 3.1.0 — the request container stops leaking into the GC |
| 2 | + |
| 3 | +Every request used to leave a reference cycle behind. `di_middleware` binds the |
| 4 | +`Request` into the container's context and then stored the container back into |
| 5 | +that same request, so the container — and the request and every dependency |
| 6 | +resolved during it — could only be reclaimed by the garbage collector, never by |
| 7 | +reference counting. |
| 8 | + |
| 9 | +**No public API change.** `FromDI`, `fetch_di_container`, |
| 10 | +`fetch_request_container`, `inject`, `setup_di`, `aiohttp_request_provider` and |
| 11 | +`aiohttp_websocket_provider` keep their signatures and behavior. |
| 12 | + |
| 13 | +## Fix |
| 14 | + |
| 15 | +- **The request container is removed from the request when the handler |
| 16 | + returns**, in a `finally` so it also runs when the handler raises. The |
| 17 | + container was already bounded by the `async with` around it, so nothing may |
| 18 | + read the entry after that block — the entry outliving the request was the bug. |
| 19 | + |
| 20 | + Measured over 100 requests against a real server, GC disabled during the run: |
| 21 | + |
| 22 | + | modern-di | this integration | cyclic objects per request | |
| 23 | + |---|---|---| |
| 24 | + | 3.1.1 | 3.0.0 | 21.9 | |
| 25 | + | **3.1.1** | **3.1.0** | **0** | |
| 26 | + |
| 27 | + Bare aiohttp with no DI produces 0, so all of them were ours. |
| 28 | + |
| 29 | + There were two independent cycles and both had to go. The other one — every |
| 30 | + `Container` storing itself in its own `_scope_map` — was fixed upstream in |
| 31 | + [modern-di 3.1.1](https://github.com/modern-python/modern-di/releases/tag/3.1.1). |
| 32 | + Neither fix alone moves the number, which is why this release requires it. |
| 33 | + |
| 34 | +## Packaging |
| 35 | + |
| 36 | +- **`modern-di` floor raised to `>=3.1.1,<4`** (from `>=3,<4`). Required, not |
| 37 | + cosmetic: this release's regression test fails against modern-di 3.0.0, which |
| 38 | + the old range permitted, so leaving the floor alone would claim support for a |
| 39 | + version the suite does not pass on. |
| 40 | + |
| 41 | +## Feature |
| 42 | + |
| 43 | +- **A canonical `examples/app.py`** — a minimal runnable aiohttp app wiring |
| 44 | + modern-di end to end, covered by a test so it cannot rot. |
| 45 | + |
| 46 | +## Downstream |
| 47 | + |
| 48 | +Upgrading pulls modern-di 3.1.1 or newer. No code change needed on your side; |
| 49 | +no API of this package moved. |
| 50 | + |
| 51 | +If you read `request[_CONTAINER_REQUEST_KEY]` directly rather than through |
| 52 | +`FromDI` / `fetch_request_container`, note that the entry is now gone once the |
| 53 | +handler returns. Reading it during the request is unchanged. |
| 54 | + |
| 55 | +## Why this was found |
| 56 | + |
| 57 | +An audit of all 12 modern-di integrations that build per-request containers, |
| 58 | +prompted by the upstream 3.1.1 fix. Two were affected: this one and |
| 59 | +`modern-di-starlette` (fixed in its 3.1.0). Both shared the same signature — |
| 60 | +the container stashed inside the very request object its own context points at. |
| 61 | +The other ten are clean: `fastapi`, `litestar` and `taskiq` expose the container |
| 62 | +through a yield dependency and never store it back; `flask`, `aiogram` and |
| 63 | +`faststream` store it somewhere the context cannot reach; `celery`, `arq` and |
| 64 | +`typer` pass no context at all; `grpc` keeps the child in a contextvar the |
| 65 | +`ServicerContext` cannot reach. |
| 66 | + |
| 67 | +## Internals |
| 68 | + |
| 69 | +- 12 tests, 100% line coverage, Python 3.10–3.14; `ruff`, `ty` clean. |
0 commit comments