dmontagu / fastapi-utils

Reusable utilities for FastAPI

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[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 GETs:

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