Skip to content

Fix bug: Sub app with custom openapi#4657

Closed
LorhanSohaky wants to merge 29 commits intofastapi:masterfrom
LorhanSohaky:bugfix/subapp-custom-openapi
Closed

Fix bug: Sub app with custom openapi#4657
LorhanSohaky wants to merge 29 commits intofastapi:masterfrom
LorhanSohaky:bugfix/subapp-custom-openapi

Conversation

@LorhanSohaky
Copy link
Contributor

Close #4656

@codecov
Copy link

codecov bot commented Apr 18, 2022

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (cf73051) 100.00% compared to head (8296742) 100.00%.
Report is 1082 commits behind head on master.

❗ Current head 8296742 differs from pull request most recent head 6158f8d. Consider uploading reports for the commit 6158f8d to get more accurate results

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.
📢 Have feedback on the report? Share it here.

@github-actions
Copy link
Contributor

📝 Docs preview for commit 3f65720 at: https://625d5babd3dd483d26c00844--fastapi.netlify.app

@github-actions
Copy link
Contributor

📝 Docs preview for commit 40a1171 at: https://625e64c8d3aa855b35f602d3--fastapi.netlify.app

@LorhanSohaky LorhanSohaky force-pushed the bugfix/subapp-custom-openapi branch from 40a1171 to 9c9066d Compare April 19, 2022 07:31
@github-actions
Copy link
Contributor

📝 Docs preview for commit 9c9066d at: https://625e65fb379bb750e4f58fa5--fastapi.netlify.app

@github-actions
Copy link
Contributor

📝 Docs preview for commit a8baa03 at: https://62d0c49592a7c02b652fb805--fastapi.netlify.app

@github-actions
Copy link
Contributor

📝 Docs preview for commit 8296742 at: https://62d747b1b37fce40e36eec2b--fastapi.netlify.app

@LorhanSohaky
Copy link
Contributor Author

@tiangolo , can you review?

@LorhanSohaky LorhanSohaky changed the title bugfix/subapp-custom-openapi Fix bug: Sub app with custom openapi Oct 4, 2022
@github-actions
Copy link
Contributor

📝 Docs preview for commit b881e95 at: https://639ccb54c1ba73100925960e--fastapi.netlify.app

@github-actions
Copy link
Contributor

📝 Docs preview for commit 1c6281d at: https://63a5985de1cfd468dc801fb0--fastapi.netlify.app

@tiangolo
Copy link
Member

📝 Docs preview for commit 09497db at: https://64848a9eaab7f46b156614be--fastapi.netlify.app

@tiangolo
Copy link
Member

📝 Docs preview for commit 488bd15 at: https://64865ebb6038ce684e4f639a--fastapi.netlify.app

@tiangolo
Copy link
Member

📝 Docs preview for commit b9fbd27 at: https://6491dc4fd8f6f600c9c4503d--fastapi.netlify.app

@grreeenn
Copy link

grreeenn commented Aug 3, 2023

I see it. Messes with client generator

@LorhanSohaky
Copy link
Contributor Author

This problem prevents the generation of protected documentation

Copy link
Member

@YuriiMotov YuriiMotov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@YuriiMotov
Copy link
Member

This problem prevents the generation of protected documentation

Could you please elaborate on this? Can you give an example?

@tiangolo
Copy link
Member

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 include_router, not mounts.

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! 🍰

@tiangolo tiangolo closed this Jul 26, 2025
@LorhanSohaky
Copy link
Contributor Author

@YuriiMotov

Let me describe my problem better...
I have an API with several sub-apps within it. Some of these sub-apps have public documentation, while others only allow access to documentation through authentication.
When the documentation for these sub-apps is generated, it doesn't include the subpath where it's located, so when I try to execute a request through Swagger, it gives an error.

@YuriiMotov
Copy link
Member

@YuriiMotov

Let me describe my problem better... I have an API with several sub-apps within it. Some of these sub-apps have public documentation, while others only allow access to documentation through authentication. When the documentation for these sub-apps is generated, it doesn't include the subpath where it's located, so when I try to execute a request through Swagger, it gives an error.

Could you provide a minimal example of such app?

@LorhanSohaky
Copy link
Contributor Author

@YuriiMotov
Let me describe my problem better... I have an API with several sub-apps within it. Some of these sub-apps have public documentation, while others only allow access to documentation through authentication. When the documentation for these sub-apps is generated, it doesn't include the subpath where it's located, so when I try to execute a request through Swagger, it gives an error.

Could you provide a minimal example of such app?

Of course!

Public doc:
image

Private doc:

image image

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}

@YuriiMotov
Copy link
Member

YuriiMotov commented Jul 27, 2025

@LorhanSohaky, thanks for the code example!

The problem here is related to the fact that you are overriding the openapi.json endpoint, and your implementation is simplified compare to default implementation.

If you look at the default implementation of openapi.json endpoint, you will see that before calling get_openapi (it's inside self.openapi()) it updates servers by adding url equal to root_path.

if self.openapi_url:
urls = (server_data.get("url") for server_data in self.servers)
server_urls = {url for url in urls if url}
async def openapi(req: Request) -> JSONResponse:
root_path = req.scope.get("root_path", "").rstrip("/")
if root_path not in server_urls:
if root_path and self.root_path_in_servers:
self.servers.insert(0, {"url": root_path})
server_urls.add(root_path)
return JSONResponse(self.openapi())
self.add_route(self.openapi_url, openapi, include_in_schema=False)

The implementation shown in docs misses this part. It works well for apps that are mounted at '/', but fails in other case.

So, to fix your problem you need to change the implementation of /openapi.json endpoint to something like:

@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.
Thanks for bringing this to our attention!

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong path at sub app with custom open api

8 participants