Fix bug: Sub app with custom openapi#4657
Fix bug: Sub app with custom openapi#4657LorhanSohaky wants to merge 29 commits intofastapi:masterfrom
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #4657 +/- ##
==========================================
Coverage 100.00% 100.00%
==========================================
Files 540 533 -7
Lines 13969 13701 -268
==========================================
- Hits 13969 13701 -268 ☔ View full report in Codecov by Sentry. |
|
📝 Docs preview for commit 3f65720 at: https://625d5babd3dd483d26c00844--fastapi.netlify.app |
|
📝 Docs preview for commit 40a1171 at: https://625e64c8d3aa855b35f602d3--fastapi.netlify.app |
40a1171 to
9c9066d
Compare
|
📝 Docs preview for commit 9c9066d at: https://625e65fb379bb750e4f58fa5--fastapi.netlify.app |
|
📝 Docs preview for commit a8baa03 at: https://62d0c49592a7c02b652fb805--fastapi.netlify.app |
|
📝 Docs preview for commit 8296742 at: https://62d747b1b37fce40e36eec2b--fastapi.netlify.app |
|
@tiangolo , can you review? |
|
📝 Docs preview for commit b881e95 at: https://639ccb54c1ba73100925960e--fastapi.netlify.app |
|
📝 Docs preview for commit 1c6281d at: https://63a5985de1cfd468dc801fb0--fastapi.netlify.app |
|
📝 Docs preview for commit 09497db at: https://64848a9eaab7f46b156614be--fastapi.netlify.app |
|
📝 Docs preview for commit 488bd15 at: https://64865ebb6038ce684e4f639a--fastapi.netlify.app |
|
📝 Docs preview for commit b9fbd27 at: https://6491dc4fd8f6f600c9c4503d--fastapi.netlify.app |
|
I see it. Messes with client generator |
|
This problem prevents the generation of protected documentation |
YuriiMotov
left a comment
There was a problem hiding this comment.
@LorhanSohaky, thanks for your efforts!
I think the situation described in #8614 is not a bug, but expected behavior (see my reply there).
And there is no need to change something.
Could you please elaborate on this? Can you give an example? |
|
As @YuriiMotov says, this is expected behavior, if you want to have multiple path operations in a single OpenAPI, they should be in the same FastAPI app, using Mounts are actually a lower level feature from Starlette that is not related to OpenAPI and how FastAPI connects everything together. For now, I'll pass on this one, but thanks for the interest! 🍰 |
|
Let me describe my problem better... |
Could you provide a minimal example of such app? |
Of course! Private doc:
Notice that in the image above, the curl command does not include the path where the sub-app is located. from typing import Union, Annotated
from fastapi import FastAPI, Depends
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from fastapi.security import HTTPBasic, HTTPBasicCredentials
security = HTTPBasic()
app = FastAPI(
title="EXAMPLE",
version="0.1.0",
)
private_subapp = FastAPI(
title="EXAMPLE PRIVATE",
version="0.1.0",
docs_url=None,
redoc_url=None,
openapi_url=None,
)
@private_subapp.get("/openapi.json")
async def openapi(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
return get_openapi(
title=f"PRIVATE: OpenAPI",
version="0.1.0",
routes=private_subapp.routes,
)
@private_subapp.get("/docs")
async def get_documentation(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
return get_swagger_ui_html(
openapi_url="/private/openapi.json",
title=f"PRIVATE: Swagger webhook docs",
)
@private_subapp.post("/items/{item_id}")
async def create_item(
item_id: int,
item: str,
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
):
return {"item_id": item_id, "item": item}
app.mount("/private", private_subapp)
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q} |
|
@LorhanSohaky, thanks for the code example! The problem here is related to the fact that you are overriding the If you look at the default implementation of fastapi/fastapi/applications.py Lines 999 to 1011 in d5d302c The implementation shown in docs misses this part. It works well for apps that are mounted at So, to fix your problem you need to change the implementation of @private_subapp.get("/openapi.json")
async def openapi(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
req: Request,
):
# The following 3 lines may be moved to lifespan event
app = cast(FastAPI, req.app)
urls = (server_data.get("url") for server_data in app.servers)
server_urls = {url for url in urls if url}
root_path = req.scope.get("root_path", "").rstrip("/")
if root_path not in server_urls:
if root_path and app.root_path_in_servers:
app.servers.insert(0, {"url": root_path})
server_urls.add(root_path)
return get_openapi(
title=f"PRIVATE: OpenAPI",
version="0.1.0",
routes=private_subapp.routes,
servers=app.servers, # <- added
)I think we should probably mention this somewhere in docs. |



Close #4656