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: OpenAPI schema generation for handler <...> detected multiple parameters named <...> with different types #3889

Open
1 of 4 tasks
Molozey opened this issue Dec 5, 2024 · 8 comments · May be fixed by #3890
Open
1 of 4 tasks
Labels
Bug 🐛 This is something that is not working as expected

Comments

@Molozey
Copy link

Molozey commented Dec 5, 2024

Description

Hello everyone! I encountered a behavior, and I'm not sure if it's correct.

The reproduction steps are attached in the form of a repository (the following description will refer to it).

As a demonstration, I wanted to create BasicViewProvider, which is a simple key-value pair and used as dependency at Controller

class BasicViewProvider(ABC):
    """
    Abstract View Provider for demo key-value
    """

    @abstractmethod
    def put(self, key: str, value: str) -> None: ...

    @abstractmethod
    def get(self, key: str) -> str: ...

    @abstractmethod
    def delete(self, key: str) -> bool: ...
class ExampleController(Controller):
    """
    Example Controller
    """

    path = "/example"
    tags = ["Example"]

    # If we remove dependencies examples: all is OK!
    dependencies = {"view_repo": Provide(view_provider_factory)}

    @post(path="/put")  # If we remove dependencies examples: all is OK!
    async def put(
        self,
        idx: str,
        obj_id: str,
        view_repo: BasicViewProvider
    ) -> None:
        view_repo.put(idx, obj_id)
      ...

At the same time, my path group requires additional parameters (in the real task, to provide object access models via middleware). So, I register the group as

example_route = Router(
    path="/objects",
    route_handlers=[ExampleController],
    parameters={
        # For simple parameter we can provide examples to solve problem
        "team_id": Parameter(
            str,
            description="Affected team id",
            required=True,
            # examples=[
            #     Example(
            #         value="mocked-team-id-uuid",
            #         summary="summary",
            #         description="descr",
            #         external_value="Test-team-id",
            #     )
            # ],
        ),

        # For cookie parameter examples will not help
        "x-session-token": Parameter(
            str,
            description="Session JWT token",
            cookie="x-session-token",
            required=False,
            # examples=[]
        ),
    },
    # middleware=[auth_middleware()],     # Assume that we need parameters to auth and permissions
)

The handler calls work correctly and do what they should, however, after I enter Swagger, I get an error:

500: OpenAPI schema generation for handler `app.controllers.example.ExampleController.hidden` detected multiple parameters named 'x-session-token' with different types.

It's important to note that when the application starts, I add example creation in the OpenAPI configuration.

If I turn off example generation, the error disappears.

def app():

    return Litestar(
        debug=True,
        route_handlers=[example_route],
        openapi_config=OpenAPIConfig(
            title="API",
            version=Configuration.VERSION,
            description="API",
            path="/docs",
            create_examples=True,  # If we remove create examples: all is OK!
        ),
        on_startup=[__startup],
        on_shutdown=[_shutdown],
    )

If I disable dependency injection, the error disappears.

class ExampleController(Controller):
    """
    Example Controller
    """

    path = "/example"
    tags = ["Example"]

    # If we remove dependencies examples: all is OK!
    # dependencies = {"view_repo": Provide(view_provider_factory)}

    @post(path="/put")  # If we remove dependencies examples: all is OK!
    async def put(
        self,
        idx: str,
        obj_id: str,
        # view_repo: BasicViewProvider
    ) -> None:
        view_repo.put(idx, obj_id)

    @post(path="/delete")
    async def delete(
        self,
        idx: str,
        # view_repo: BasicViewProvider   # If we remove dependencies examples: all is OK!
    ) -> None:
        view_repo.delete(idx)

    @get(path="/get")
    async def get(
        self,
        idx: str,
        # view_repo: BasicViewProvider  # If we remove dependencies examples: all is OK!
    ) -> None:
        view_repo.get(key=idx)

If I set an example parameter when registering the group, the error disappears, but I was unable to set an example for a cookie-type parameter (Swagger will open, but gives infinite spinner after clicking at route).

example_route = Router(
    path="/objects",
    route_handlers=[ExampleController],
    parameters={
        # For simple parameter we can provide examples to solve problem
        "team_id": Parameter(
            str,
            description="Team ID",
            required=True,
            examples=[
                Example(
                    value="mocked-team-id-uuid",
                    summary="summary",
                    description="descr",
                    external_value="Test-team-id",
                )
            ],
        ),

        # For cookie parameter examples will not help
        "x-session-token": Parameter(
            str,
            description="Session JWT token",
            cookie="x-session-token",
            required=False,
            examples=[Example(value="mocked-session")]
        ),
    },
    # middleware=[auth_middleware()],     # Assume that we need parameters to auth and permissions
)

Next, I started investigating the cause of this behavior and found that multiple parameter declarations are being called because the examples in the Schema type differ, even when all other fields match.

litestar/_openapi/parameters.py

        pre_existing = self._parameters[(parameter.name, parameter.param_in)]
        if parameter == pre_existing: # <--------- Not equals then different examples at schema
            return
        
        # Add this block to understand what is different
        for key, val in parameter.__dict__.items():
            print(
                "Equals for key={}: {}".format(
                    key, val == pre_existing.__getattribute__(key)
                )
            )
            if key == "schema":
                for schema_key, schema_value in val.__dict__.items():
                    print(
                        "[Schema Field] key={}: {}".format(
                            schema_key,
                            schema_value
                            == pre_existing.__getattribute__(key).__getattribute__(
                                schema_key
                            ),
                        )
                    )
        raise ImproperlyConfiguredException(
            f"OpenAPI schema generation for handler `{self.route_handler}` detected multiple parameters named "
            f"'{parameter.name}' with different types."
        )

Which gives me next results

Equals for key=name: True
Equals for key=param_in: True
Equals for key=schema: False
[Schema Field] key=all_of: True
[Schema Field] key=any_of: True
[Schema Field] key=one_of: True
[Schema Field] key=schema_not: True
[Schema Field] key=schema_if: True
[Schema Field] key=then: True
[Schema Field] key=schema_else: True
[Schema Field] key=dependent_schemas: True
[Schema Field] key=prefix_items: True
[Schema Field] key=items: True
[Schema Field] key=contains: True
[Schema Field] key=properties: True
[Schema Field] key=pattern_properties: True
[Schema Field] key=additional_properties: True
[Schema Field] key=property_names: True
[Schema Field] key=unevaluated_items: True
[Schema Field] key=unevaluated_properties: True
[Schema Field] key=type: True
[Schema Field] key=enum: True
[Schema Field] key=const: True
[Schema Field] key=multiple_of: True
[Schema Field] key=maximum: True
[Schema Field] key=exclusive_maximum: True
[Schema Field] key=minimum: True
[Schema Field] key=exclusive_minimum: True
[Schema Field] key=max_length: True
[Schema Field] key=min_length: True
[Schema Field] key=pattern: True
[Schema Field] key=max_items: True
[Schema Field] key=min_items: True
[Schema Field] key=unique_items: True
[Schema Field] key=max_contains: True
[Schema Field] key=min_contains: True
[Schema Field] key=max_properties: True
[Schema Field] key=min_properties: True
[Schema Field] key=required: True
[Schema Field] key=dependent_required: True
[Schema Field] key=format: True
[Schema Field] key=content_encoding: True
[Schema Field] key=content_media_type: True
[Schema Field] key=content_schema: True
[Schema Field] key=title: True
[Schema Field] key=description: True
[Schema Field] key=default: True
[Schema Field] key=deprecated: True
[Schema Field] key=read_only: True
[Schema Field] key=write_only: True
[Schema Field] key=examples: False <-------------------- Only one DIFF which breaks all
[Schema Field] key=discriminator: True
[Schema Field] key=xml: True
[Schema Field] key=external_docs: True
[Schema Field] key=example: True
Equals for key=description: True
Equals for key=required: True
Equals for key=deprecated: True
Equals for key=allow_empty_value: True
Equals for key=style: True
Equals for key=explode: True
Equals for key=allow_reserved: True
Equals for key=example: True
Equals for key=examples: True
Equals for key=content: True

So my general question, is there any reasons that we need to check examples equality? Because if i ignore that field: all is OK!

URL to code causing the issue

https://github.com/Molozey/litestar-params-demo

MCVE

# Your MCVE code here

Steps to reproduce

1. Go to litestar-params-demo (reproduction repo url)
2. Run with run.py
3. Open localhost:24500/docs/swagger
4. See error
5. Set create_examples=False (run:32)
6. Open localhost:24500/docs/swagger
7. See NO error

(7.5) Try other methods described at issue

Screenshots

No response

Logs

No response

Litestar Version

litestar==2.13.0

Platform

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

Note

While we are open for sponsoring on GitHub Sponsors and
OpenCollective, we also utilize Polar.sh to engage in pledge-based sponsorship.

Check out all issues funded or available for funding on our Polar.sh dashboard

  • If you would like to see an issue prioritized, make a pledge towards it!
  • We receive the pledge once the issue is completed & verified
  • This, along with engagement in the community, helps us know which features are a priority to our users.
Fund with Polar
@Molozey Molozey added the Bug 🐛 This is something that is not working as expected label Dec 5, 2024
@Molozey Molozey linked a pull request Dec 5, 2024 that will close this issue
@Molozey
Copy link
Author

Molozey commented Dec 5, 2024

For some reason adding
__eq__ method for Schema class solves the problem

@provinzkraut
Copy link
Member

So my general question, is there any reasons that we need to check examples equality?

Yes, because this means there is a conflict, which Litestar cannot resolve on its own. If you define the same parameter with different examples, those are, as far as the OpenAPI schema is concerned, different definitions. That fact that they only differ in the examples doesn't matter much.


Can you provide a simple, self-contained example that reproduces the behaviour you're describing? With the example repository I'm not able to reproduce the error you're referencing.

@Molozey
Copy link
Author

Molozey commented Dec 11, 2024

Yes, this is a small example

import uvicorn
from litestar import Controller
from litestar import Litestar
from litestar import post
from litestar import Router
from litestar.di import Provide
from litestar.openapi import OpenAPIConfig
from litestar.params import Parameter
from pydantic import BaseConfig


class Configuration(BaseConfig):
    PORT: int = 24500

    VERSION: str = "v0.0.1"


class SessionTempViewProvider:
    storage: dict[str, str]

    def put(self, key: str, value: str) -> None:
        self.storage[key] = value


async def view_provider_factory() -> SessionTempViewProvider:
    return SessionTempViewProvider()


class ExampleController(Controller):
    """
    Example Controller
    """

    path = "/example"
    tags = ["Example"]

    # If we remove dependencies examples: all is OK!
    dependencies = {"view_repo": Provide(view_provider_factory)}

    @post(path="/put")  # If we remove dependencies examples: all is OK!
    async def put(
        self, idx: str, obj_id: str, view_repo: SessionTempViewProvider
    ) -> None:
        view_repo.put(idx, obj_id)


example_route = Router(
    path="/objects",
    route_handlers=[ExampleController],
    parameters={
        # For simple parameter we can provide examples to solve problem
        "team_id": Parameter(
            str,
            description="Team ID",
            required=True,
        ),
        # For cookie parameter examples will not help
        "x-session-token": Parameter(
            str,
            description="Session JWT token",
            cookie="x-session-token",
            required=False,
        ),
    },
    # middleware=[auth_middleware()],     # Assume that we need parameters to auth and permissions
)


def app():

    return Litestar(
        debug=True,
        route_handlers=[example_route],
        openapi_config=OpenAPIConfig(
            title="API",
            version=Configuration.VERSION,
            description="API",
            path="/docs",
            create_examples=True,  # If we remove create examples: all is OK!
        ),
    )


def main():
    """Run auth service"""
    uvicorn.run(
        "app.run:app",
        host="0.0.0.0",
        port=Configuration.PORT,
        reload=True,
    )


if __name__ == "__main__":
    main()

http://0.0.0.0:24500/docs/swagger
image
image

@provinzkraut
Copy link
Member

provinzkraut commented Dec 11, 2024

Okay so here is a minimal reproducer:

from litestar import Litestar
from litestar import post
from litestar.openapi import OpenAPIConfig
from litestar.params import Parameter


async def provider() -> str:
    return "hello"


@post(path="/")
async def handler(dep: str) -> None:
    pass


app = Litestar(
    debug=True,
    route_handlers=[handler],
    dependencies={"dep": provider},
    parameters={"team_id": Parameter()},
    openapi_config=OpenAPIConfig(
        title="API",
        version="",
        create_examples=True,
    ),
)

app.openapi_schema

@Molozey
Copy link
Author

Molozey commented Dec 11, 2024

For some reasons i cannot run your example
ValueError: Unable to configure handler 'queue_listener'

@provinzkraut
Copy link
Member

That's an unrelated issue with your Python version + how you're running it. See #3647

@Molozey
Copy link
Author

Molozey commented Dec 11, 2024

Thanks you, as quick solution without changing python version i make this, and get OpenAPI error about different types

def app():
    _lite_app = Litestar(
        debug=True,
        route_handlers=[handler],
        dependencies={"dep": provider},
        parameters={"team_id": Parameter()},
        openapi_config=OpenAPIConfig(
            title="API",
            version="0.0.1",
            path="/docs",
            create_examples=True,
        ),
    )
    print(_lite_app.openapi_schema)
    return _lite_app


def main():
    uvicorn.run(
        f"__main__:app",
        host="0.0.0.0",
        port=24500,
        reload=True,
    )


if __name__ == "__main__":
    main()
    ```

@Molozey
Copy link
Author

Molozey commented Dec 11, 2024

I'm not sure is it possible to break something, but adding to litestar.openapi.spec.schema.Schema (#3890) solves the problem

    def __eq__(self, other: object):
        return super().__eq__(other)  

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
Development

Successfully merging a pull request may close this issue.

2 participants