Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Double-nested router can't have empty path(despite having prefix at grandparent level)#8058

Unanswered
teuneboon asked this question inQuestions
Discussion options

Describe the bug
If I have a router setup like this:main router -> sub router -> sub sub router. And I includesub router with a prefix, I can't have an empty path parameter on any routes in thesub sub router.

To Reproduce
Steps to reproduce the behavior:

  1. Create a file with double-nested routers:
fromfastapiimportAPIRoutersub_sub_router=APIRouter()sub_router=APIRouter()main_router=APIRouter()main_router.include_router(sub_router,prefix='/foo')sub_router.include_router(sub_sub_router)
  1. Add a path operation function without a path:
@sub_sub_router.get('')deftest():return'bar'
  1. See error:Exception: Prefix and path cannot be both empty (path operation: test)

Expected behavior
Since at one point in the path there is a prefix, "test" should be served at /foo.

Additional context
This is probably a choice to make on whether you want to support this use-case. To explain why I do it like this: we have a pretty complex API, and to reduce complexity(and prevent files being thousands of lines long) each endpoint(i.e.: /users) has a folder structure like:

  • users/
    • __init__.py <-- contains main users router, includes read & write.py's routers
    • read.py <-- contains a router with get/read actions for users
    • write.py <-- contains a router with write actions for users
  • routes.py <-- includes the user router from users/__init__.py and sets the prefix to '/users'

So for my usecase I have 2 alternative ways to work around this issue:

  • Include read/write directly in the routes.py(this removes some abstraction though, maybe at one point we'll split it even more than just read/write)
  • Set the prefix in users/__init__.py to '/users'(this moves the route definitions to a bunch of different files which is kind of meh)

I'm perfectly fine with using one of these workarounds if I have to, but before I do I wanted to know if you see this behavior as a bug as well. I checked out pull request#415 to see if I could make a pull request that fixes this, but unfortunately I don't know how to reach the parent router in the code that was changed there.

You must be logged in to vote

Replies: 10 comments 3 replies

Comment options

@teuneboon

Here's a workaround that I think handles your issue pretty cleanly:

@sub_sub_router.get("$")deftest():return'bar'

Why this works: the path passed to the route decorator here is actually used to build a regex.$ matches the end of a string, so"$" is basically equivalent to"", but the recently-modified router-inclusion-check just checks that the path is not empty (not that it starts with "/"). So it will not raise an error when you do the nested include. (Actually, starlette adds a$ to the path already when building the regex, but having a second$ doesn't change what matches.)

Note: this will prevent the check that the route starts with a "/" when you are done cascading your inclusions, so you'd need to take responsibility for that. But that seems like a pretty reasonable compromise given the design you are trying to achieve.

You must be logged in to vote
2 replies
@mmzeynalli
Comment options

As of today, this does not work. You need to send a request to$, otherwise, the route is not found. Any suggestions?

@gertzgal
Comment options

@mmzeynalli Same here

Comment options

@dmontagu thanks, that should fix the issue for me.

You must be logged in to vote
0 replies
Comment options

Thanks for the help here@dmontagu ! 👏 🙇

Thanks for reporting back and closing the issue@teuneboon 👍

You must be logged in to vote
0 replies
Comment options

@teuneboon@tiangolo@dmontagu the generated swagger shows /route$ instead of /route for this approach... and calls the route that is suffixed with the dollar sign when you click "try it out". Is there a way to avoid this?

You must be logged in to vote
0 replies
Comment options

@HansBrende I'm not sure I understand what you mean. If you still have problems please create a new issue with a simple self-contained example that shows how to reproduce the error, that way we can help you.

You must be logged in to vote
0 replies
Comment options

@tiangolo I am referring to the approach suggested by dmontagu. It works, but the Open API (swagger) page displays the regex in the path and the "try it out" button calls the incorrect API path. Not sure what part of this you didn't understand, perhaps you can elaborate.

You must be logged in to vote
0 replies
Comment options

Hello there,

I had the same issue as@HansBrende regarding the generated OpenAPI schemas (also mentioned as "swagger" in his comment).

@dmontagu's answer - with"$" is just the right way. But, for those wondering about how to get the trailing$ away from the schema paths description here is what I found out.

I took the time to search for "OpenAPI" in the documentation and ended in theExtending OpenAPI chapter. I leave to you the lecture of the chapter but if we take the final example - as of today:

fromfastapiimportFastAPIfromfastapi.openapi.utilsimportget_openapiapp=FastAPI()@app.get("/items/")asyncdefread_items():return [{"name":"Foo"}]defcustom_openapi():ifapp.openapi_schema:returnapp.openapi_schemaopenapi_schema=get_openapi(title="Custom title",version="2.5.0",description="This is a very custom OpenAPI schema",routes=app.routes,    )openapi_schema["info"]["x-logo"]= {"url":"https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"    }app.openapi_schema=openapi_schemareturnapp.openapi_schemaapp.openapi=custom_openapi

A simple change of thecustom_openapi() function as follow fix our issue:

defcustom_openapi():ifapp.openapi_schema:returnapp.openapi_schemaopenapi_schema=get_openapi(title="Custom title",version="2.5.0",description="This is a very custom OpenAPI schema",routes=app.routes,    )openapi_schema["info"]["x-logo"]= {"url":"https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"    }## Change starts here.forpathin {xforxinapp.openapi_schema["paths"]ifx.endswith("$")}:app.openapi_schema["paths"][path[:-1]]=app.openapi_schema["paths"][path]delapp.openapi_schema["paths"][path]### Change ends here.app.openapi_schema=openapi_schemareturnapp.openapi_schema

I suppose that this is the right way but@tiangolo may correct me here.

Thanks for everything@dmontagu@tiangolo.

You must be logged in to vote
0 replies
Comment options

Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs.

You must be logged in to vote
0 replies
Comment options

@tiangolo I wrote under answer a year ago, so Im writing here again. The problem persists, and adding$ sign (as per@dmontagu) does not solve the problem, API route is WITH$ sign, and any API without$ does not exist. Any other workaround? Or plan to fix this?

P. S. My version: 0.115.14

You must be logged in to vote
0 replies
Comment options

Code example in initial post is invalid. Simplified correct code example:

fromfastapiimportAPIRouter,FastAPIrouter=APIRouter()@router.get("")deftest():return"bar"app=FastAPI()app.include_router(router)

So, it's not about "double-nested" router, but just about router without prefix

You must be logged in to vote
1 reply
@mmzeynalli
Comment options

In my case I have:

fromfastapiimportAPIRouterfrom .action_portfolioimportrouterasaction_routerfrom .general_portfolioimportrouterasgeneral_routerfrom .review_portfolioimportrouterasreview_routerfrom .update_portfolioimportrouterasupdate_routerrouters= [update_router,action_router,review_router,general_router]router=APIRouter()forrinrouters:router.include_router(r,prefix='/portfolios')

And in general router:

router=APIRouter(prefix='',tags=['User Portfolios'])@router.get('',summary='Get portfolio list',)asyncdeffunc():pass

Because of this, I had to write:

fromfastapiimportAPIRouterfrom .action_portfolioimportrouterasaction_routerfrom .general_portfolioimportrouterasgeneral_routerfrom .review_portfolioimportrouterasreview_routerfrom .update_portfolioimportrouterasupdate_routerrouters= [update_router,action_router,review_router]# FIXME: As prefix and route cannot be both empty string, we had to do# such workaround. The solution mentioned in https://github.com/tiangolo/fastapi/discussions/8618# is not valid anymore, and I have added comment. We are waiting for an answerrouter=APIRouter()forrinrouters:router.include_router(r,prefix='/portfolios')router.include_router(general_router,prefix='')

and

router=APIRouter(prefix='/portfolios',tags=['User Portfolios'])@router.get('',summary='Get portfolio list',)asyncdeffunc():pass
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
8 participants
@teuneboon@tiangolo@HansBrende@funilrys@mmzeynalli@dmontagu@YuriiMotov@gertzgal
Converted from issue

This discussion was converted from issue #510 on February 28, 2023 11:55.


[8]ページ先頭

©2009-2025 Movatter.jp