fix: @inject works on HTTPEndpoint / WebSocketEndpoint methods - #28
Merged
Merged
Conversation
The wrapper took exactly one positional argument, so bound as a method it received self and the connection and failed with `TypeError: takes 1 positional argument but 2 were given` on the first request. It now forwards *args / **kwargs unchanged and finds the connection as the first positional Request or WebSocket, so a function endpoint and a method of a class-based endpoint go through the same decorator, including WebSocketEndpoint.on_receive / on_disconnect with their trailing arguments. A handler that receives no connection fails with a TypeError naming the handler. Closes #18
The org standard keeps a decision record to one paragraph and reserves them for decisions that are hard to reverse. The inject passthrough is neither; its rejected alternatives live in the PR body.
This was referenced Sep 15, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #18.
What was wrong
@injectreturnedasync def wrapper(connection). Bound as a class attribute, Python passesselffirst, so the wrapper received two positional arguments and raised on the first request, before any DI ran:The issue describes an
AttributeErroronconnection.scope; that is not what happens. The arity check fails first. Either way it is a request-time error with nothing in the message that points at@inject, and with the scope key private (docs/adr/0002) there was no manual fallback.Decision
One
@injectfor functions and methods. The wrapper is nowasync def wrapper(*args, **kwargs): it locates the connection as the first positionalRequestorWebSocket, resolves the markers from that connection's child container, and calls the handler with everything it was given plus the resolved values. A handler that receives no connection fails with aTypeErrornaming the handler.Rejected:
inject_methoddecorator. Starlette's own hook shapes already differ (on_connect(ws),on_receive(ws, data),on_disconnect(ws, close_code)), so a method-specific decorator would still have to forward trailing arguments, at which point it is the general decorator with a second name. Flask and aiogram in this org already wrap with*args, **kwargs.TypeErrorat decoration time. A function in a class body is an ordinary function when the decorator sees it; the only signal is a first parameter namedself, which is a convention, not a fact.Finding the connection by
isinstancerather than by position is what makes the three WebSocket hooks work without special-casing.on_disconnectis safe to inject into: Starlette calls it insidedispatch'sfinally, before the ASGI app returns and the middleware deletes the scope entry.FastAPI and Litestar have no analogue: neither has a class-based path outside their native
Depends/Provide. This is Starlette-only.Tests
tests/test_endpoints.py:HTTPEndpoint.getresolving APP- and REQUEST-scoped providers plus theRequestcontext provider;WebSocketEndpoint.on_connect,on_receiveandon_disconnectresolving SESSION-scoped providers plus theWebSocketcontext provider; the no-connectionTypeError. The first two were red before the fix with the error above.test_finished_request_leaves_no_cyclic_garbageis parametrized over a function route and anHTTPEndpointroute, so the no-cyclic-garbage invariant is shown to hold on the new path rather than assumed.Coverage stays at 100%.
Docs
injectrow in the API table, plus a class-based example under Usage.docs/adr/0002: no longer describes class-based endpoints as an open gap. Both ADRs are cut to one paragraph, per the org standard fordocs/adr/.Out of scope
def geton anHTTPEndpoint.injectis async-only for function endpoints today; unchanged.modern-di.Release
Minor bump: the public
injectcontract widens.