[BUG] Unable to resolve multiple handlers for the same HTTP action but with different routes
HymanZHAN opened this issue · comments
Describe the bug
I have a BlogCBV where I have a few handler functions, three of which are GET
s:
- get_drafts
- get_blogs
- get_blog
get_drafts
has a path of blogs/drafts
get_blog
has a path of blogs/{blog_id}
However, when I want to reach the endpoint get_drafts
by hitting blogs/drafts
, get_blog
will be reached instead.
Below is the code snippet:
# blog.py
from typing import List
from fastapi import APIRouter, Depends, Path
from fastapi_utils.cbv import cbv
from fastapi_utils.inferring_router import InferringRouter
from sqlalchemy.orm import Session
from app.api.utils.db import get_db
from app.api.utils.security import get_current_active_user
from app.db import crud
from app.db.orm.blog import Blog
from app.db.orm.user import User
from app.dtos.blog import BlogForDetail, BlogForEdit, BlogForList, BlogForNew
from app.dtos.msg import Msg
router = APIRouter()
BLOG_DEFAULT_PAGE_SIZE = 10
router = InferringRouter()
@cbv(router)
class BlogCBV:
session: Session = Depends(get_db)
current_user: User = Depends(get_current_active_user)
@router.get("/drafts")
def get_drafts(
self, *, page: int = 1, page_size: int = BLOG_DEFAULT_PAGE_SIZE,
) -> List[BlogForList]:
"""
Retrieve blog drafts for the current user.
"""
drafts = crud.blog.get_drafts(
self.session, author=self.current_user, page=page, page_size=page_size
)
return [BlogForList.from_orm(draft) for draft in drafts]
@router.get("/{blog_id}")
def get_blog(
self, *, blog_id: int = Path(..., title="The ID of the blog to be returned."),
) -> BlogForDetail:
"""
Retrieve a published blog.
"""
blog = crud.blog.get(self.session, id=blog_id)
return BlogForDetail.from_orm(blog)
@router.get("/",)
def get_blogs(
self, *, page: int = 1, page_size: int = BLOG_DEFAULT_PAGE_SIZE,
) -> List[BlogForList]:
"""
Retrieve published blogs by all users.
"""
blogs = crud.blog.get_multi(self.session, page=page, page_size=page_size)
return [BlogForList.from_orm(blog) for blog in blogs]
# api.py
from fastapi import APIRouter
from app.api.api_v1.endpoints import blogs, login, users, utils, profile
api_router.include_router(blogs.router, prefix="/blogs", tags=["blogs"])
# main.py
app = FastAPI(title=config.PROJECT_NAME, openapi_url="/api/v1/openapi.json")
app.include_router(api_router, prefix=config.API_V1_STR)
Expected behavior
get_drafts
should be correctly resolved and reached.
Environment:
- OS: Linux (Fedora 31)
- FastAPI Utils, FastAPI, and Pydantic versions:
0.1.1
0.49.0
pydantic version: 1.4
pydantic compiled: True
install path: /usr/local/lib/python3.7/site-packages/pydantic
python version: 3.7.4 (default, Sep 12 2019, 15:40:15) [GCC 8.3.0]
platform: Linux-5.5.5-200.fc31.x86_64-x86_64-with-debian-10.1
optional deps. installed: ['email-validator', 'devtools']
- Python version: 3.7.4
Additional context
In my test:
def test_get_all_my_drafts(self, session, test_user_token_headers):
batch_create_random_blogs(session, is_published=False, num=3)
batch_create_random_blogs(session, is_published=True, num=2)
url = app.url_path_for("get_drafts")
r = client.get(url, headers=test_user_token_headers)
drafts = r.json()
debug(drafts)
assert r.status_code == 200
assert len(drafts) == 3
The url can be correctly resolved: url: '/api/v1/blogs/drafts'
Thanks for reporting; this must be related to the order in which endpoints are traversed when moving them over. I don't recall writing any code that could have resulted in them being reordered, but it's definitely possible. I'll take a look.
As a short term workaround you could just use a path like /blogs/{blog_id}
, but this should of course get fixed.
Thanks for making this library and thanks for looking into this. The cbv
approach can really make things quite a bit cleaner and look more RESTful so I am looking forward to using it! I can try and make a minimal repo to reproduce this problem if that would be helpful.
Found the problem -- it's because the inspect.getmembers
function returns members in alphabetical order rather than the order they were defined. There are two possible solutions: order the members in the same order as the routes in the router, or order them by their first line number. I think the first approach is preferable for various reasons, just need to see if I can come up with a clean implementation.
Should be able to fix this tonight, though I'm about to hop on a plane so might not be able to push for a couple hours.
Thank you very much for the prompt investigation! But in my use case, it's really no hurry so please take your time.
Just pushed the fix, and released v0.2.0 to PyPI which has the fix. It made some minor changes to the fastapi_utils.timing
module, making certain implementation details private rather than public, and adding docs. (Hopefully that doesn't break anything for you.)
If you run into any more related issues, or especially if the change I made doesn't fix things for your application, please let me know. And please continue to report any other bugs you find, or feature requests you have!
Thank you for the quick fix! Really appreciate it! I'll try it out and if there's anything strange I shall let you know. Cheers!