Swagger UI Path Issue
If the app is hosted behind a reverse proxy and the hosted endpoint adds a /api in front the swagger ui breaks, as it can not load the file from the root of the site:
Stripping the / from the path in swagger ui solves this by making use of the dynamic path behaviour and leads to the correct path /api/openapi.json
Patch:
diff --git a/app/backend/.venv/lib/python3.11/site-packages/quart_schema/extension.py b/app/backend/.venv/lib/python3.11/site-packages/quart_schema/extension_new.py
index adcd222a..93e73e42 100644
--- a/app/backend/.venv/lib/python3.11/site-packages/quart_schema/extension.py
+++ b/app/backend/.venv/lib/python3.11/site-packages/quart_schema/extension.py
@@ -330,7 +330,7 @@ class QuartSchema:
return await render_template_string(
SWAGGER_TEMPLATE,
title=self.info.title,
- openapi_path=self.openapi_path,
+ openapi_path=self.openapi_path.lstrip("/"),
swagger_js_url=current_app.config["QUART_SCHEMA_SWAGGER_JS_URL"],
swagger_css_url=current_app.config["QUART_SCHEMA_SWAGGER_CSS_URL"],
)
I think it would be better in this case to set the openapi_path to openapi.json or /api/openapi.json in the QuartSchema constructor.
Then the add_url_rule must add a / if not specified. Otherwise the following error will occur:
Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/gunicorn/__main__.py", line 10, in <module>
run(prog="gunicorn")
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/gunicorn/app/wsgiapp.py", line 66, in run
WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]", prog=prog).run()
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/gunicorn/app/base.py", line 235, in run
super().run()
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/gunicorn/app/base.py", line 71, in run
Arbiter(self).run()
^^^^^^^^^^^^^
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/gunicorn/arbiter.py", line 57, in __init__
self.setup(app)
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/gunicorn/arbiter.py", line 117, in setup
self.app.wsgi()
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/gunicorn/app/base.py", line 66, in wsgi
self.callable = self.load()
^^^^^^^^^^^
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/gunicorn/app/wsgiapp.py", line 57, in load
return self.load_wsgiapp()
^^^^^^^^^^^^^^^^^^^
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/gunicorn/app/wsgiapp.py", line 47, in load_wsgiapp
return util.import_app(self.app_uri)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/gunicorn/util.py", line 370, in import_app
mod = importlib.import_module(module)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1204, in _gcd_import
File "<frozen importlib._bootstrap>", line 1176, in _find_and_load
File "<frozen importlib._bootstrap>", line 1147, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 940, in exec_module
File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
File "/workspaces/project/app/backend/main.py", line 276, in <module>
app, schema = new_app()
^^^^^^^^^
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/dependency_injector/wiring.py", line 1049, in _patched
return _sync_inject(
^^^^^^^^^^^^^
File "src/dependency_injector/_cwiring.pyx", line 24, in dependency_injector._cwiring._sync_inject
File "/workspaces/project/app/backend/main.py", line 248, in new_app
schema = QuartSchema(
^^^^^^^^^^^^
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/quart_schema/extension.py", line 270, in __init__
self.init_app(app)
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/quart_schema/extension.py", line 308, in init_app
app.add_url_rule(self.openapi_path, "openapi", self.openapi)
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/flask/sansio/scaffold.py", line 47, in wrapper_func
return f(self, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/flask/sansio/app.py", line 650, in add_url_rule
rule_obj = self.url_rule_class(rule, methods=methods, **options)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/quart/routing.py", line 27, in __init__
super().__init__(
File "/workspaces/project/app/backend/.venv/lib/python3.11/site-packages/werkzeug/routing/rules.py", line 475, in __init__
raise ValueError(f"URL rule '{string}' must start with a slash.")
ValueError: URL rule 'openapi.json' must start with a slash.
I wanted to specify it for the template because we do not known which pre-path the /swagger/ endpoint will be available on when writing the code. And the resolution of resource in web browsers allows to specify relative paths, ~~which will work for all current usages~~ + the one I specified. But I can see that a current implementation specifying openapi_path=/some/root/path would break.
Otherwise specifying openapi.json as openapi_path would also raise the question of whether it would be available on all paths then (from the Developers perspective)?
I am open to implement it however you deem it more ergonomic.
Does this patch help?
diff --git i/src/quart_schema/extension.py w/src/quart_schema/extension.py
index 254b025..f6ab71f 100644
--- i/src/quart_schema/extension.py
+++ w/src/quart_schema/extension.py
@@ -7,7 +7,14 @@ from types import new_class
from typing import Any, Literal, TypeVar
import click
-from quart import Blueprint, current_app, Quart, render_template_string, ResponseReturnValue
+from quart import (
+ Blueprint,
+ current_app,
+ Quart,
+ render_template_string,
+ ResponseReturnValue,
+ url_for,
+)
from quart.cli import pass_script_info, ScriptInfo
from quart.json.provider import DefaultJSONProvider
from quart.typing import ResponseReturnValue as QuartResponseReturnValue
@@ -326,7 +333,7 @@ class QuartSchema:
return await render_template_string(
SWAGGER_TEMPLATE,
title=self.info.title,
- openapi_path=self.openapi_path,
+ openapi_path=url_for("openapi"),
swagger_js_url=current_app.config["QUART_SCHEMA_SWAGGER_JS_URL"],
swagger_css_url=current_app.config["QUART_SCHEMA_SWAGGER_CSS_URL"],
)
@@ -336,7 +343,7 @@ class QuartSchema:
return await render_template_string(
REDOC_TEMPLATE,
title=self.info.title,
- openapi_path=self.openapi_path,
+ openapi_path=url_for("openapi"),
redoc_js_url=current_app.config["QUART_SCHEMA_REDOC_JS_URL"],
)
@@ -345,7 +352,7 @@ class QuartSchema:
return await render_template_string(
SCALAR_TEMPLATE,
title=self.info.title,
- openapi_path=self.openapi_path,
+ openapi_path=url_for("openapi"),
scalar_js_url=current_app.config["QUART_SCHEMA_SCALAR_JS_URL"],
)