Skip to content

iter_route_contexts() yields an empty path for APIWebSocketRoute, so prefixed websocket routes cannot be enumerated #16244

Description

@fgarofalo56

Discussed in #15782

First Check

  • I added a very descriptive title here.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I already searched in Google "How to X in FastAPI" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to FastAPI but to Pydantic.
  • I already checked if it is not related to FastAPI but to Swagger UI.
  • I already checked if it is not related to FastAPI but to ReDoc.

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions