Document that ConnexionMiddleware might require to specifiy routes explicitly
Description
From reading the documentation, I was under the assumption that API paths defined in the OpenAPI specification don't need to be registered explicitly.
Using Starlette with the ConnexionMiddleware however, I seem to have to explicitly add Routes to each API endpoint, otherwise, the server returns 404. This is not the case for the Swagger UI, which is served correctly without being registered explicitly.
It does not seem to be caused by Connexion not finding / resolving the python functions correctly: If I don't provide a correct operationId or configure a Resolver, Connexion would warn Failed to add operation for GET /api/v1/example and raise a ResolverError when trying to access the API.
Expected behaviour
From the documentation and the fact that the Swagger UI is served automatically, I would expect the following code to return "Hello World!" when sending a GET request to /api/v1/example (OpenAPI spec can be found under "Steps to reproduce"):
from connexion import ConnexionMiddleware
from connexion.decorators import StarletteDecorator
from starlette.applications import Starlette
@StarletteDecorator()
def get_example():
return "Hello world!", 200
app = Starlette(debug=True)
app = ConnexionMiddleware(app)
app.add_api("api/openapi.yaml", base_path="/api/v1")
Actual behaviour
I need to explicitly add the route of the endpoint:
from starlette.routing import Route
...
app = Starlette(
debug=True,
routes=[
Route("/api/v1/example", get_example),
],
)
...
If this is expected behavior, maybe that necessity could be mentioned in the documentation.
Steps to reproduce
Install connexion[swagger-ui], uvicorn and starlette and run uvicorn app:app with the following files:
api/openapi.yaml
openapi: "3.0.0"
info:
version: 1.0.0
title: Example API
paths:
/example:
get:
description: An example endpoint.
operationId: app.get_example
responses:
"200":
description: Successful.
Full app.py
from connexion import ConnexionMiddleware
from connexion.decorators import StarletteDecorator
from starlette.applications import Starlette
from starlette.routing import Route
@StarletteDecorator()
def get_example():
return "Hello world!", 200
app = Starlette(
debug=True,
routes=[
Route("/api/v1/example", get_example),
],
)
app = ConnexionMiddleware(app)
app.add_api("api/openapi.yaml", base_path="/api/v1")
Remove the routes=[...] argument and try to access the example endpoint.
Additional info:
I'm new to ASGI, Starlette and Connexion, please excuse any obvious things I missed.
I am only using Starlette in the first place to host a static HTML file and some resources next to the API, because I couldn't get that working with the AsyncApp. If this is possible, feel free to point me in the right direction.
My current setup would look something like this:
app = Starlette(debug=True, routes=[
Route("/api/v1/example", get_example),
Mount("/", app=StaticFiles(directory="static", html=True), name="static"),
])
Output of the commands:
python --versionPython 3.11.6
pip show connexion | grep "^Version\:"Version: 3.0.6
Thanks for the report @abstractbyte.
Agree that we can add a note on this in the documentation.
This way of using Connexion is mostly meant if you already have a Starlette application and want to start leveraging Connexion though. If you're starting from scratch, the AsyncApp is the recommended way to go.
That being said, it's indeed not straightforward to host static files currently. For now, you should be able to do it like this:
app = AsyncApp(...)
app._middleware_app.router.mount("/", app=StaticFiles(directory="static", html=True), name="static")
We should provide a .mount() method on the AsyncApp directly to make this easier though.
Thanks for the fast reply, @RobbeSneyders! The proposed workaround via _middleware_app works well.
When I'm done with my project I'll try to look more into Connexion and maybe I'll be able to open a PR.