PWZER / swagger-ui-py

Swagger UI for Python web framework, such Tornado, Flask and Sanic. https://pwzer.github.io/swagger-ui-py/
https://pypi.org/project/swagger-ui-py
Apache License 2.0
69 stars 31 forks source link

Not sure if this is a bug. CSS/js in production redirect to "base url" #34

Open fithisux opened 2 years ago

fithisux commented 2 years ago

I use swagger-ui-py==21.12.8 and locally Openapi documentation is created without problems and I can enjoy it at

http://localhost:8005/api/doc (8005 is custom).

We deploy in our preview envs, and this url works fine

https://preview-newe-2253-a9wd53-debug.preview.gfknewron.com/insight/additive-decomposition/api/doc/swagger.json

But htis one

https://preview-newe-2253-a9wd53-debug.preview.gfknewron.com/insight/additive-decomposition/api/doc/

has problems with resources

GET https://preview-newe-2253-a9wd53-debug.preview.gfknewron.com/api/doc/static/swagger-ui.css net::ERR_ABORTED 404

it should have been

https://preview-newe-2253-a9wd53-debug.preview.gfknewron.com/insight/additive-decomposition/api/doc/static/swagger-ui.css

The urls are generated from our deployment scripts.

Is there something I can do? Is this a misuse of the library on my part?

Any idea what is missing?

Python 3.10.3 on MacOS Big sur, x86-64, installed through pyenv. flask==2.1.0

fithisux commented 2 years ago

My init

def init():
    app = Flask(__name__)
    app.config["JSON_SORT_KEYS"] = False
    app.config.update(settings)
    app.errorhandler(Exception)(exceptions_handler)

    metrics = GunicornInternalPrometheusMetrics.for_app_factory(defaults_prefix=NO_PREFIX)
    metrics.init_app(app)

    app.before_request(log_request_start)
    app.after_request(log_request_end)

    app.add_url_rule(rule="/actuator/health", endpoint="healthcheck", view_func=healthcheck, methods=["GET"])
    app.add_url_rule(rule="/v1/additive_decomposition/summary", endpoint="/v1/additive_decomposition/summary", view_func=additive_decomposition_v1_proxy, methods=["POST"])
    logging.config.dictConfig(app.config.get("LOGGING"))

    spec = APISpec(title="Additive Decomposition API", version="0.0.1", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()])

    with app.test_request_context():
        spec.path(view=additive_decomposition_v1_proxy)
        spec.path(view=healthcheck)

    with open(DOC_DIR, "w") as f:
        json.dump(spec.to_dict(), f, indent=2)
    api_doc(app, config_path=DOC_DIR, url_prefix="/api/doc", title="API doc")

    return app
PWZER commented 2 years ago

@fithisux It seems that a proxy is added before calling the api_doc function, and its uri is /insight/additive-decomposition, but this proxy does not work for /api/doc/static/swagger-ui.css.

fithisux commented 2 years ago

Thank you @PWZER. So, this issue is outside my application. Correct?

PWZER commented 2 years ago

@fithisux I'm guessing you might need to use like this

api_doc(app, config_path=DOC_DIR, url_prefix="/insight/additive-decomposition/api/doc", title="API doc")
PWZER commented 2 years ago

@fithisux Could be the problem caused by this

spec.path(view=additive_decomposition_v1_proxy)
dulanthaf commented 2 years ago

@fithisux were you able to resolve this issue?

fithisux commented 2 years ago

I went to a different solution @dulanthaf . I used apispec. https://apispec.readthedocs.io/en/latest/

Even there I had a poblem since routes were not rewritten correctly. So I downloaded in a separate folder, called "static"

the contents of https://github.com/swagger-api/swagger-ui.

Also there I added an empty "init.py"

Now I did in my code:


spec = APISpec(title="Additive Decomposition API", version="0.0.1", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()])

    with app.test_request_context():
        spec.path(view=xxx1)
        spec.path(view=xxx2)
        spec.path(view=healthcheck)

    with open(OPENAPI_FILE, "w") as f:
        json.dump(spec.to_dict(), f, indent=2)

where (I actually dumped the json in the static folder with the rest of swagger ui js)

STATIC_DIR = os.path.dirname(static.__file__)
OPENAPI_FILE = f"{STATIC_DIR}/swagger.json"

now flask can add a route to static as

app = Flask(__name__, static_url_path="/static", static_folder=STATIC_DIR)

dulanthaf commented 2 years ago

I'm not sure if it's the same setup, but we use nginx for the proxy setup. So what worked for me was, add the proxy path, in your case probably url_prefix="/insight/additive-decomposition/api/doc" that includes the proxy subdirectory, and then in the nginx config, add a new location entry for the server:

location ^~ /insight/additive-decomposition/api/doc/ {
    proxy_pass http://something:port/insight/additive-decomposition/api/doc/;
}
fithisux commented 2 years ago

Thank you @dulanthaf this is exactly what I suspected, but our SREs did not like this solution. Thank you anyway.

dulanthaf commented 2 years ago

@fithisux any particular reason why the SRE's did not like the solution? I would assume it's one more entry into the existing proxy pass configuration?

Olegt0rr commented 9 months ago

@fithisux any particular reason why the SRE's did not like the solution? I would assume it's one more entry into the existing proxy pass configuration?

Because it's workaround, which need to be described and supported. So really good SRE should avoid this solution

Olegt0rr commented 9 months ago

@fithisux it happens, since template uses prefix as an absolute path

https://github.com/PWZER/swagger-ui-py/blob/21c62701c12d7a5b47f340c1631500cdaa5ef00f/swagger_ui/templates/doc.html#L4-L11

I.e. paths are looks like /api/doc/static/swagger-ui.css

Any external path override will cause this issue:

Screenshot 2024-02-20 at 11 14 17

Making static path relative to current swagger-ui location should help to avoid this behaviour and be independent from any location overrides

  <head>
    <meta charset="UTF-8">
    <title> {{ title }} </title>
    <link rel="stylesheet" type="text/css" href="{{ relative_prefix }}/static/swagger-ui.css" />
    <link rel="stylesheet" type="text/css" href="{{ relative_prefix }}/static/index.css" />
    <link rel="icon" type="image/png" href="{{ relative_prefix }}/static/favicon-32x32.png" sizes="32x32" />
    <link rel="icon" type="image/png" href="{{ relative_prefix }}/static/favicon-16x16.png" sizes="16x16" />
  </head>

So it should be rendered like this

  <head>
    <meta charset="UTF-8">
    <title> {{ title }} </title>
    <link rel="stylesheet" type="text/css" href="doc/static/swagger-ui.css" />
    <link rel="stylesheet" type="text/css" href="doc/static/index.css" />
    <link rel="icon" type="image/png" href="doc/static/favicon-32x32.png" sizes="32x32" />
    <link rel="icon" type="image/png" href="doc/static/favicon-16x16.png" sizes="16x16" />
  </head>
Olegt0rr commented 9 months ago

@fithisux ,

I've made my suggestions with PR mentioned above.

But if you want workaround right now:

class ApplicationDocumentRelative(ApplicationDocument):
    @property
    def doc_html(self):
        doc_location_split = self.url_prefix.split("/")
        static_relative_location = doc_location_split.pop()
        self.set_relative_swagger_url()
        return self.env.get_template("doc.html").render(
            url_prefix=static_relative_location,
            title=self.title,
            config_url=self.swagger_json_uri_absolute,
            parameters=self.parameters,
            oauth2_config=self.oauth2_config,
        )

    def set_relative_swagger_url(self):
        split_path = self.swagger_json_uri_absolute.split("/")
        swagger_url_relative = "/".join(split_path[-2:])
        self.parameters["url"] = f'"{swagger_url_relative}"'

def api_doc(app, **kwargs):
    doc = ApplicationDocumentRelative(app, **kwargs)

    handler = doc.match_handler()

    if not handler:
        raise RuntimeError("No match application isinstance type!")

    if not callable(handler):
        raise RuntimeError("handler is callable required!")

    return handler(doc)

Result:

Screenshot 2024-02-20 at 12 49 21
fithisux commented 8 months ago

Thank you very much. I do not work in this project anymore.