Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions docs/adr/0001-pure-asgi-middleware.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# The container is opened in pure ASGI middleware, not `BaseHTTPMiddleware`

**Decision:** `_DIMiddleware` is a plain ASGI callable, not a `BaseHTTPMiddleware` subclass.
`_DIMiddleware` is a plain ASGI callable rather than a `BaseHTTPMiddleware` subclass.
`BaseHTTPMiddleware` is the ergonomic choice, but it runs the downstream app in a separate anyio
task, so `contextvars` set around `call_next` are not visible in the endpoint, and it handles `http`
only, so a `Scope.SESSION` child for a WebSocket would need a second mechanism. The plain callable
costs about fifteen lines of raw-protocol handling and in return has the ASGI `scope` dict in hand,
which is how the child container reaches `@inject` without a `contextvar`. **Revisit trigger:**
Starlette ships a supported middleware base that shares context with the downstream app *and*
covers `websocket` scopes; either half alone leaves this integration writing the raw protocol.
task, so `contextvars` set around `call_next` are not visible in the endpoint, and it handles
`http` scopes only, so the `Scope.SESSION` child a WebSocket needs would take a second mechanism.
The plain callable costs about fifteen lines of raw-protocol handling and in return holds the ASGI
`scope` dict, which is how the child container reaches `@inject` without a `contextvar`. That trade
only flips if Starlette ships a supported middleware base that both shares context with the
downstream app and covers `websocket` scopes; either half alone leaves this integration writing the
raw protocol anyway.
21 changes: 10 additions & 11 deletions docs/adr/0002-child-container-stays-internal.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# The per-connection child container has no public accessor

**Decision:** the child container is reachable only through `@inject` + `FromDI`; the ASGI scope
key it lives under stays private and there is no `fetch_di_child_container(connection)`. The
middleware deletes its scope entry in a `finally` when the connection ends, because the container's
context holds the connection, the connection owns the `scope` dict, and the dict held the container:
a cycle per request that left finished requests to the garbage collector. An accessor advertises the
entry as something a caller may hold, and any holder that outlives the connection either revives the
cycle or reads a deleted entry, so there is no version of it that is both safe and useful. Class-based
`HTTPEndpoint` / `WebSocketEndpoint` were once the gap this left; `@inject` now binds as a method
(modern-python/modern-di-starlette#28), so it was a missing decorator path, not a missing accessor.
**Revisit trigger:** a use for the child container appears outside the connection's own call stack;
the lifetime question above has to be answered first, and the answer is the accessor's contract.
The child container is reachable only through `@inject` and `FromDI`: the ASGI scope key it lives
under stays private, and there is no `fetch_di_child_container(connection)` beside the root
container's `fetch_di_container(app)`. The middleware deletes its scope entry in a `finally` when
the connection ends, because the container's context holds the connection, the connection owns the
`scope` dict, and the dict held the container: a cycle per request that left finished requests to
the garbage collector. An accessor advertises that entry as something a caller may hold, and a
holder that outlives the connection either revives the cycle or reads a deleted entry, so no
version of it is both safe and useful. Class-based `HTTPEndpoint` and `WebSocketEndpoint` were once
the gap this left, but `@inject` now binds as a method, so that was a missing decorator path rather
than a missing accessor.
Loading