1010from functools import lru_cache
1111from typing import Any
1212
13- from fastapi import FastAPI , Response
13+ from fastapi import FastAPI , Response , routing
1414from fastapi .openapi .docs import get_redoc_html , get_swagger_ui_html
1515from fastapi .openapi .utils import get_openapi
1616from fastapi .responses import HTMLResponse
2424_SPEC_URL = "/openapi.json"
2525
2626
27+ def _iter_routes (app : FastAPI ) -> list [Any ]:
28+ """Return the app routes flattened, one entry per operation.
29+
30+ Since FastAPI 0.141 / Starlette 1.6, ``include_router`` stores a single lazy
31+ ``_IncludedRouter`` in ``app.routes`` instead of flattening the child routes,
32+ so iterating ``app.routes`` directly no longer yields the individual
33+ ``APIRoute`` objects. ``iter_route_contexts`` resolves those into contexts
34+ that expose the effective path and tags (router prefix and tags merged in),
35+ and ``get_openapi`` accepts them in place of routes. Older FastAPI versions
36+ allowed by our version constraint lack that helper but already flatten the
37+ routes, so fall back to plain iteration there.
38+ """
39+ iter_route_contexts = getattr (routing , "iter_route_contexts" , None )
40+ if iter_route_contexts is None : # pragma: no cover - FastAPI < 0.141
41+ return list (app .routes )
42+ return list (iter_route_contexts (app .routes ))
43+
44+
45+ def _is_internal (route : Any ) -> bool :
46+ """Return whether a route (or route context) is tagged as internal."""
47+ original = getattr (route , "original_route" , route )
48+ return isinstance (original , APIRoute ) and INTERNAL_TAG in (getattr (route , "tags" , None ) or [])
49+
50+
2751def _remove_422 (schema : dict [str , Any ]) -> None :
2852 for method_item in schema .get ("paths" , {}).values ():
2953 for param in method_item .values ():
3054 param .get ("responses" , {}).pop ("422" , None )
3155
3256
3357def _build_default_schema (app : FastAPI ) -> dict [str , Any ]:
34- routes = [r for r in app . routes if not ( isinstance ( r , APIRoute ) and INTERNAL_TAG in r . tags )]
58+ routes = [r for r in _iter_routes ( app ) if not _is_internal ( r )]
3559 tags = [t for t in (app .openapi_tags or []) if t .get ("name" ) != INTERNAL_TAG ]
3660 schema = get_openapi (
3761 title = app .title ,
@@ -50,7 +74,7 @@ def _build_default_schema(app: FastAPI) -> dict[str, Any]:
5074
5175
5276def _build_internal_schema (app : FastAPI ) -> dict [str , Any ]:
53- routes = [r for r in app . routes if isinstance ( r , APIRoute ) and INTERNAL_TAG in r . tags ]
77+ routes = [r for r in _iter_routes ( app ) if _is_internal ( r ) ]
5478 tags = [t for t in (app .openapi_tags or []) if t .get ("name" ) == INTERNAL_TAG ]
5579 schema = get_openapi (
5680 title = f"{ app .title } - Internal" ,
0 commit comments