reboot: bind an app_internal route's context to its endpoint - #171
Draft
rjhuijsman wants to merge 1 commit into
Draft
rjhuijsman wants to merge 1 commit into
rjhuijsman wants to merge 1 commit into
Conversation
An HTTP route that opts into an app-internal `ExternalContext` via
`app_internal=True` must receive one for every request it handles,
whatever the shape of its path, and no other route may receive one.
Before this change, `PythonWebFramework.HTTP._api_route` recorded an
app-internal route's path string in a set, and the middleware that
puts the context on `request.state` looked the request's path up in
that set before routing. A route whose path carries parameters (e.g.
`/__/things/{id}`) therefore never matched its own requests — the set
held the template while requests carried concrete paths — so such a
route silently got the external context it had not asked for and
failed later on the trusted call it was meant to make. Pattern
matching the paths in the middleware would not have been a fix
either: the middleware runs before routing and cannot know which
endpoint Starlette will dispatch to, so a `{param}` pattern would have
granted the app-internal context to every ordinary route under the
same prefix as well.
The grant is now bound to the endpoint rather than to the path:
`APIRoute` carries an `app_internal` flag, and when the server process
registers such a route it attaches a FastAPI route-level dependency
that replaces the request's context with an app-internal one. That
dependency runs after Starlette has dispatched to the endpoint and
before the endpoint itself, so it reaches exactly that endpoint's
requests. The middleware now always installs the external context.
Handlers keep reading `request.state.reboot_external_context`, and
existing `app_internal=True` routes need no change.
The new `tests/reboot/aio/http_app_internal_test.py` covers a static
`app_internal=True` route, a parameterized one (which failed before
this change), and a route under the same prefix registered without
`app_internal` (which must keep its external context).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8vHXG2KLHDUruv8642AXY
Current Aviator status
This pull request is currently open (not queued). How to mergeTo merge this PR, comment
See the real-time status of this PR on the
Aviator webapp.
Use the Aviator Chrome Extension
to see the status of your PR within GitHub.
|
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.
Important
This PR was written by an AI agent and has not yet been reviewed by a human. It is awaiting the author's review before peer review.
Why
A custom HTTP route can opt into an app-internal
ExternalContext(one carrying the application'scaller_id, able to call app-internal-only servicers) by registering withapp_internal=True. That grant must reach every request the route handles, whatever the shape of its path, and must not reach any other route.Before this change,
PythonWebFramework.HTTP._api_routerecorded an app-internal route's path string in a set, and theexternal_context_middlewarelooked the request's path up in that set — before routing — to decide which context to put onrequest.state.reboot_external_context. A route whose path carries parameters (e.g./__/things/{id}) therefore never matched its own requests: the set held the template while requests carried concrete paths, so such a route silently got the external context it had not asked for and failed later on the trusted call it was meant to make.Pattern matching the paths in the middleware (Starlette's
compile_path) would not have been a fix either: the middleware runs before routing and cannot know which endpoint Starlette will dispatch to, so a{param}pattern would also have granted the app-internal context to every ordinary route living under the same prefix (e.g./__/things/health).What
The grant is bound to the endpoint rather than to the path:
PythonWebFramework.APIRoutegains anapp_internal: bool = Falsefield;_api_routepopsapp_internalfrom the kwargs and stores it there._app_internal_pathsis gone.fastapi.add_api_route(...)), an app-internal route gets a FastAPI route-level dependency (dependencies=[Depends(...)]) that setsrequest.state.reboot_external_contextto an app-internal context. A route-level dependency runs after Starlette has dispatched to that endpoint and before the endpoint itself, and it is prepended to any dependencies the route declared itself, so the endpoint's ownInjectExternalContextalready sees the app-internal context. TheDependsobject is created in the server process, so nothing new needs pickling.request.state.reboot_external_contextunchanged (reboot/mcp/context.py, the OAuth handlers), and the existingapp_internal=Trueusers (oauth_server.py,oauth_providers.py) need no change.Tests
New
tests/reboot/aio/http_app_internal_test.py(target//tests/reboot/aio:http_app_internal_test_py) brings anApplicationup withreboot.aio.tests.Rebootand hits three routes over HTTP; each handler reports whether the context it was handed carries acaller_id:/__/test/staticregistered withapp_internal=Truegets an app-internal context (regression check; already true before this change)./__/test/{item}registered withapp_internal=Truegets an app-internal context (failed before this change)./__/test/plain, under the same prefix but registered withoutapp_internal, gets an external context (guards against the wildcard-leak approach).Before (tests on top of unmodified
main):After (this commit):
mypy reboot/aio/http.py tests/reboot/aio/http_app_internal_test.pyreports only the five pre-existing PyJWTbytes/strerrors inoauth_server.py/tests.py.The OAuth tests that exercise the existing
app_internal=Trueroutes (tests/reboot/aio/auth:oauth_providers_test_pyand siblings) depend on//reboot/ping, which does not build from themonoworkspace the change was tested in (a pre-existingpydantic_to_zodrelative-importTypeError), so they are left to this PR's CI.Relation to #109
This is orthogonal to and was split out of #109, which had briefly tried pattern-matching in the middleware, then a registration-time refusal of parameterized paths, and now leaves this to this PR.
🤖 Generated with Claude Code
https://claude.ai/code/session_01D8vHXG2KLHDUruv8642AXY