Discussed in #15782
First Check
Description
iter_route_contexts() — the helper added in 0.137.2 (#15785) as the supported replacement for walking router.routes — yields path == "" for APIWebSocketRoute, while HTTP routes in the same router get their include prefix applied correctly.
The mounted path is not recoverable from anything the returned context exposes: ctx.path and ctx.path_format are both "", and ctx.route.path carries only the route's own un-prefixed path. So for an app that mounts websockets under a prefix, iter_route_contexts() cannot enumerate them at all.
This matters because iter_route_contexts() is the migration path being recommended to everyone whose route-introspection broke in 0.137 (instrumentation, metrics middleware, route-manifest generators, "is this endpoint mounted?" tests). Code that migrates to it as directed silently loses its websocket routes — the same failure mode as the original regression, just narrowed.
Example Code
from fastapi import APIRouter, FastAPI, WebSocket
from fastapi.routing import iter_route_contexts
router = APIRouter()
@router.get("/items")
def items():
return {}
@router.websocket("/stream/{job_id}")
async def stream(websocket: WebSocket, job_id: str):
await websocket.accept()
app = FastAPI()
app.include_router(router, prefix="/api/v1")
for ctx in iter_route_contexts(app.routes):
route = getattr(ctx, "route", None)
print(f"{ctx.path!r:<28} {type(route).__name__:<22} {getattr(route, 'path', None)!r}")
Operating System
Windows
FastAPI Version
0.141.1
Python Version
3.13.14
Additional Context
Output of the snippet above:
'/openapi.json' Route '/openapi.json'
'/docs' Route '/docs'
'/docs/oauth2-redirect' Route '/docs/oauth2-redirect'
'/redoc' Route '/redoc'
'/api/v1/items' APIRoute '/items'
'' APIWebSocketRoute '/stream/{job_id}'
Expected for the last row: '/api/v1/stream/{job_id}', matching how /api/v1/items is resolved one row above.
Full context object for the websocket entry, showing the path is genuinely absent rather than stored elsewhere:
ctx.path = ''
ctx.path_format = ''
ctx.name = ''
ctx.methods = set()
ctx.route = APIWebSocketRoute(path='/stream/{job_id}', ...)
ctx.original_route = APIWebSocketRoute(path='/stream/{job_id}', ...)
ctx.endpoint = None
The prefix does exist at _IncludedRouter.include_context.prefix, but _RouterIncludeContext is private, so there is no supported way for a caller to reassemble the path themselves.
Possibly related but a different surface: #16176 / #16051 concern request.scope["route"].path at request time (and note websockets retained prefixes there, while HTTP routes lost them). This report is about introspection time via iter_route_contexts(), where the asymmetry runs the other way.
Happy to open a PR if the maintainers agree on the intended behaviour — my read is that the websocket branch of the context-building should apply the include prefix the same way the HTTP branch does.
Discussed in #15782
First Check
Description
iter_route_contexts()— the helper added in 0.137.2 (#15785) as the supported replacement for walkingrouter.routes— yieldspath == ""forAPIWebSocketRoute, while HTTP routes in the same router get their include prefix applied correctly.The mounted path is not recoverable from anything the returned context exposes:
ctx.pathandctx.path_formatare both"", andctx.route.pathcarries only the route's own un-prefixed path. So for an app that mounts websockets under a prefix,iter_route_contexts()cannot enumerate them at all.This matters because
iter_route_contexts()is the migration path being recommended to everyone whose route-introspection broke in 0.137 (instrumentation, metrics middleware, route-manifest generators, "is this endpoint mounted?" tests). Code that migrates to it as directed silently loses its websocket routes — the same failure mode as the original regression, just narrowed.Example Code
Operating System
Windows
FastAPI Version
0.141.1
Python Version
3.13.14
Additional Context
Output of the snippet above:
Expected for the last row:
'/api/v1/stream/{job_id}', matching how/api/v1/itemsis resolved one row above.Full context object for the websocket entry, showing the path is genuinely absent rather than stored elsewhere:
The prefix does exist at
_IncludedRouter.include_context.prefix, but_RouterIncludeContextis private, so there is no supported way for a caller to reassemble the path themselves.Possibly related but a different surface: #16176 / #16051 concern
request.scope["route"].pathat request time (and note websockets retained prefixes there, while HTTP routes lost them). This report is about introspection time viaiter_route_contexts(), where the asymmetry runs the other way.Happy to open a PR if the maintainers agree on the intended behaviour — my read is that the websocket branch of the context-building should apply the include prefix the same way the HTTP branch does.