quart-schema icon indicating copy to clipboard operation
quart-schema copied to clipboard

Swagger UI Path Issue

Open DanielHabenicht opened this issue 9 months ago • 3 comments

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:

Image

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"],
         )

DanielHabenicht avatar Jul 17 '25 14:07 DanielHabenicht

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.

pgjones avatar Dec 02 '25 21:12 pgjones

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.

DanielHabenicht avatar Dec 04 '25 13:12 DanielHabenicht

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"],
         )

pgjones avatar Dec 14 '25 11:12 pgjones