Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: pagination DI seems broken with 2.1.0 #2358

Closed
1 of 4 tasks
nabnux opened this issue Sep 25, 2023 · 3 comments · Fixed by #2360
Closed
1 of 4 tasks

Bug: pagination DI seems broken with 2.1.0 #2358

nabnux opened this issue Sep 25, 2023 · 3 comments · Fixed by #2360
Labels
Bug 🐛 This is something that is not working as expected

Comments

@nabnux
Copy link

nabnux commented Sep 25, 2023

Description

Hi, I'm trying to bump Litestar to 2.1.0 on a project and I'm getting a 500 / Internal Server Error on endpoints implementing pagination (it was working fine before the bump). I've reproduced this behavior in the MCVE below, and provided the error message in the logs.

The error message apparently refers to a validation issue of the injected dependency used to provide the limit and offset parameters.

The other endpoint in the MCVE that gets a single user works fine btw.

URL to code causing the issue

No response

MCVE

from __future__ import annotations

from typing import TYPE_CHECKING
from typing import Self

from litestar import Litestar
from litestar import get
from litestar.contrib.sqlalchemy.base import BigIntBase
from litestar.contrib.sqlalchemy.plugins import (
    SQLAlchemySyncConfig,
    SQLAlchemyPlugin,
)
from litestar.contrib.sqlalchemy.repository import SQLAlchemySyncRepository
from litestar.controller import Controller
from litestar.di import Provide
from litestar.pagination import OffsetPagination
from litestar.params import Parameter
from litestar.repository.filters import LimitOffset

from sqlalchemy import create_engine
from sqlalchemy.orm import Mapped, Session, sessionmaker


class User(BigIntBase):
    __tablename__ = "users"

    mail: Mapped[str]


class UserRepository(SQLAlchemySyncRepository[User]):
    model_type = User


def provide_users_repo(db_session: Session) -> UserRepository:
    return UserRepository(session=db_session)


def provide_limit_offset_pagination(
    limit: int | None = Parameter(
        query="limit",
        default=None, ge=1,
        required=False,
    ),
    offset: int | None = Parameter(
        query="offset",
        default=None,
        ge=0,
        required=False,
    ),
) -> LimitOffset:
    return LimitOffset(limit, offset)


class UserController(Controller):
    dependencies = {
        "limit_offset": Provide(provide_limit_offset_pagination, sync_to_thread=True),
        "users_repo": Provide(provide_users_repo, sync_to_thread=True),
    }

    @get(path="/users", sync_to_thread=True)
    def list_users(
        self: Self,
        users_repo: UserRepository,
        limit_offset: LimitOffset,
    ) -> OffsetPagination[User]:
        users, total = users_repo.list_and_count(limit_offset)
        return OffsetPagination[User](
            items=users,
            total=total,
            limit=limit_offset.limit,
            offset=limit_offset.offset,
        )

    @get(path="/users/{user_id:int}", sync_to_thread=True)
    def get_user(
        self: Self,
        users_repo: UserRepository,
        user_id: int,
    ) -> User:
        return users_repo.get(user_id)


database_config = SQLAlchemySyncConfig(
    connection_string="sqlite:///poc.sqlite",
)
class DatabaseConfig:
    def __init__(self: Self) -> None:
        self.engine = create_engine("sqlite:///poc.sqlite")
        self.session_factory = sessionmaker(self.engine, expire_on_commit=False)
        self.sqla_config = SQLAlchemySyncConfig(
            engine_instance=self.engine,
            session_maker=self.session_factory,
        )

    def create_all(self: Self) -> None:
        with self.engine.begin() as connection:
            BigIntBase.metadata.create_all(connection)


db_config = DatabaseConfig()
app = Litestar(
    route_handlers=[UserController],
    on_startup=[db_config.create_all],
    plugins=[SQLAlchemyPlugin(config=db_config.sqla_config)],
)

Steps to reproduce

1. run the app with the provided MCVE
2. do a GET request on `/users` with or without the `limit` and `offset` parameters

Screenshots

No response

Logs

$ curl 'http://127.0.0.1:8000/users'
Traceback (most recent call last):
  File "/home/nabnux/.cache/pypoetry/virtualenvs/litestar-poc-UgV0M29C-py3.11/lib/python3.11/site-packages/litestar/_signature/model.py", line 190, in parse_values_from_connection_kwargs
    return convert(kwargs, cls, strict=False, dec_hook=deserializer, str_keys=True).to_dict()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
msgspec.ValidationError: Expected `object`, got `LimitOffset` - at `$.limit_offset`

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/nabnux/.cache/pypoetry/virtualenvs/litestar-poc-UgV0M29C-py3.11/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/nabnux/.cache/pypoetry/virtualenvs/litestar-poc-UgV0M29C-py3.11/lib/python3.11/site-packages/litestar/routes/http.py", line 79, in handle
    response = await self._get_response_for_request(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/nabnux/.cache/pypoetry/virtualenvs/litestar-poc-UgV0M29C-py3.11/lib/python3.11/site-packages/litestar/routes/http.py", line 131, in _get_response_for_request
    response = await self._call_handler_function(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/nabnux/.cache/pypoetry/virtualenvs/litestar-poc-UgV0M29C-py3.11/lib/python3.11/site-packages/litestar/routes/http.py", line 160, in _call_handler_function
    response_data, cleanup_group = await self._get_response_data(
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/nabnux/.cache/pypoetry/virtualenvs/litestar-poc-UgV0M29C-py3.11/lib/python3.11/site-packages/litestar/routes/http.py", line 194, in _get_response_data
    parsed_kwargs = route_handler.signature_model.parse_values_from_connection_kwargs(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/nabnux/.cache/pypoetry/virtualenvs/litestar-poc-UgV0M29C-py3.11/lib/python3.11/site-packages/litestar/_signature/model.py", line 203, in parse_values_from_connection_kwargs
    raise cls._create_exception(messages=messages, connection=connection) from e
litestar.exceptions.http_exceptions.InternalServerException: 500: Internal Server Error

Litestar Version

2.1.0

Platform

  • Linux
  • Mac
  • Windows
  • Other (Please specify in the description above)

Funding

  • If you would like to see an issue prioritized, make a pledge towards it!
  • We receive the pledge once the issue is completed & verified
Fund with Polar
@nabnux nabnux added Bug 🐛 This is something that is not working as expected Triage Required 🏥 This requires triage labels Sep 25, 2023
@provinzkraut provinzkraut removed the Triage Required 🏥 This requires triage label Sep 25, 2023
@provinzkraut
Copy link
Member

Here's an MCVE to reproduce:

from __future__ import annotations

from litestar import Litestar
from litestar import get
from litestar.contrib.sqlalchemy.plugins import SQLAlchemySyncConfig, SQLAlchemyInitPlugin
from litestar.repository.filters import LimitOffset
from litestar.testing import TestClient


async def provide_limit_offset_pagination() -> LimitOffset:
    return LimitOffset(limit=0, offset=0)


@get(
    path="/",
    dependencies={"limit_offset": provide_limit_offset_pagination},
)
async def handler(limit_offset: LimitOffset) -> None:
    return None


app = Litestar(
    route_handlers=[handler],
    plugins=[SQLAlchemyInitPlugin(SQLAlchemySyncConfig(connection_string="sqlite:///"))],
    debug=True,
)

with TestClient(app=app) as client:
    assert client.get("/").status_code == 200

It only happens with the SQLAlchemyInitPlugin. The serliazation plugin is fine.

@provinzkraut
Copy link
Member

@nabnux I found the source of the issue, and I have a fix pending.

For now you can use this workaround:

Instead of

from litestar.repository.filters import LimitOffset

do

from advanced_alchemy.filters import LimitOffset

The fix will make the regular imports work again.

@nabnux
Copy link
Author

nabnux commented Sep 26, 2023

Thanks for working on this and for the workaround!

provinzkraut added a commit that referenced this issue Sep 30, 2023
…rom advanced-alchemy

Signed-off-by: Janek Nouvertné <[email protected]>
cofin added a commit that referenced this issue Sep 30, 2023
…rom advanced-alchemy (#2360)

* fix(repositories): fix #2358 by re-exporting filters and exceptions from advanced-alchemy

Signed-off-by: Janek Nouvertné <[email protected]>

* fix docs

Signed-off-by: Janek Nouvertné <[email protected]>

* fix: pin adv alchemy to v0.2.2

* Update pyproject.toml

* update poetry.lock

Signed-off-by: Janek Nouvertné <[email protected]>

---------

Signed-off-by: Janek Nouvertné <[email protected]>
Co-authored-by: Cody Fincher <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug 🐛 This is something that is not working as expected
Projects
None yet
2 participants