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

Swagger UI path not using url_prefix #517

Open
blondowski opened this issue Aug 28, 2018 · 26 comments
Open

Swagger UI path not using url_prefix #517

blondowski opened this issue Aug 28, 2018 · 26 comments

Comments

@blondowski
Copy link

blondowski commented Aug 28, 2018

In our code:
app = Flask(name)
blueprint = Blueprint('api',name,url_prefix='/api')
api = Api(blueprint, doc='/doc/')
app.register_blueprint(blueprint)

From the log..it's not using the prefix...swaggerui is off root.

10.2.229.197 - - [28/Aug/2018:16:13:36 +0000] "GET /swaggerui/bower/swagger-ui/dist/fonts/DroidSans-Bold.ttf HTTP/1.1" 200 0 "http://10.99.72.221:5002/api/doc/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8"

It's causing issues for AWS Application Load Balancer, as the only route I can define is /api, since it's running inside a Docker container.

@austinjp
Copy link

austinjp commented Sep 10, 2018

EDIT: On reflection, perhaps I've misunderstood. I thought you wanted to change where assets were being delivered from, which might be possible with what I've written below. But it seems that instead your blueprint is not registering at the desired URL.

Perhaps there's an answer in here: https://flask-restplus.readthedocs.io/en/stable/scaling.html#multiple-apis-with-reusable-namespaces

Original response below.

You may want to try something like the following:

from flask import render_template
from bs4 import BeautifulSoup
import werkzeug.exceptions

def custom_ui(self):
    """Override for custom UI"""
    if self._doc_view:
        return self._doc_view()
    elif not self._doc:
        self.abort(werkzeug.exceptions.NotFound)
    r = render_template("swagger-ui.html", title=self.title, specs_url=self.specs_url)
    soup = BeautifulSoup(r, 'lxml')
    # do things with BeautifulSoup here
    return str(soup)
Api.render_doc = custom_ui
api = Api(blueprint, doc='/doc/)

It's briefly mentioned in the docs. I cobbled together the above after a spot of Googling. I'm sure it could be much improved.

@blondowski
Copy link
Author

blondowski commented Sep 25, 2018

Thanks. Yes, I do want to change where the assets are delivered from:
instead of /swaggerui/bower/swagger-ui/dist/fonts/DroidSans-Bold.ttf
they need to come from:
/api/swaggerui/bower/swagger-ui/dist/fonts/DroidSans-Bold.ttf

Our load balancer doesn't know how to route /swaggerui.

@jpowie01
Copy link

jpowie01 commented Jan 2, 2019

Hi @blondowski,
Did you manage to resolve your issue? It seems that I've got the same one on my side and haven't find any good solution yet.

Thanks in advance!

@blondowski
Copy link
Author

blondowski commented Jan 2, 2019

No sir/mam. I've tried multiple things and couldn't get it to work.

@asarchami
Copy link

I am having the same issue here. swaggerui redirects to hostname withouth url_prefix

@jpowie01
Copy link

jpowie01 commented Jan 3, 2019

I've found a little bit "hacky" work around. To fix this issue you need to derive from Api class and override _register_apidoc method which will register your custom apidoc.Apidoc definition.

With below snippet, SwaggerUI is served under /api/service/v1 path:

class MyCustomApi(Api):
    def _register_apidoc(self, app: Flask) -> None:
        conf = app.extensions.setdefault('restplus', {})
        custom_apidoc = apidoc.Apidoc('restplus_doc', 'flask_restplus.apidoc',
                                      template_folder='templates', static_folder='static',
                                      static_url_path='/api/service/v1')

        @custom_apidoc.add_app_template_global
        def swagger_static(filename: str) -> str:
            return url_for('restplus_doc.static', filename=filename)

        if not conf.get('apidoc_registered', False):
            app.register_blueprint(custom_apidoc)
        conf['apidoc_registered'] = True


blueprint = Blueprint('my_blueprint', __name__, url_prefix='/api/service/v1')
api = MyCustomApi(blueprint, version='1.0', title='My Custom API', description='My Custom API.',
                  validate=True)

@asarchami
Copy link

asarchami commented Jan 3, 2019

with this I can change the path but still cant find the static files.

@jpowie01
Copy link

jpowie01 commented Jan 3, 2019

Strange... I tested it today with Nginx as a reverse proxy without any changes in configuration. Maybe this comment (#223 (comment)) will help you somehow?

Also, be sure to clear cache in your browser - I've cached myself on that :)

@asarchami
Copy link

asarchami commented Jan 3, 2019

My environment is strange. we are using haproxy and I don't have access to it and our urls are like http://something.something:portnumber/<app_name>/<app_routes>. Originally it changed the static path to http://something.something:portnumber/<app_routes> which removed the <app_name>. now path looks OK but there is nothing there!

@jpowie01
Copy link

jpowie01 commented Jan 3, 2019

Play with swagger_static function an try to find proper filename for you:

@custom_apidoc.add_app_template_global
def swagger_static(filename: str) -> str:
    return url_for('restplus_doc.static', filename=filename)

Also, try a solution from this comment: #262 (comment).

@asarchami
Copy link

After lots of trials and errors I managed to fix the static paths right, now I get 404 for swagger.json is there a way to specify the path for that file?

@jpowie01
Copy link

jpowie01 commented Jan 4, 2019

I think this one may help you: #223 (comment). But I'm not sure about it... 🤔

@asarchami
Copy link

Didn't work. It is so frustrating! I can change all the paths but as soon as I do I loose track of the satics.

@Nachtfeuer
Copy link

I think the problem is in the api.py in this method

    def _register_apidoc(self, app):
        conf = app.extensions.setdefault('restplus', {})
        if not conf.get('apidoc_registered', False):
            app.register_blueprint(apidoc.apidoc)
        conf['apidoc_registered'] = True

What is missing is the url_prefix; it's not used here and you also cannot specify one. Basically when I change my REST API to start with something like /api/v1 ... I can manage to have /api/v1/doc to present the Swagger UI but those settings are not taken into account for that method shown above.

I don't like the idea to hack something so the right implementation is appreciated ...

@blondowski
Copy link
Author

Any updates on this?

@Kampe
Copy link

Kampe commented Sep 3, 2019

Has this been prioritized, the workaround is not ideal

@giovanniattina
Copy link

giovanniattina commented Sep 4, 2019

did anyone get any hack or which stage the implementation is?

@giovanniattina
Copy link

giovanniattina commented Sep 5, 2019

I did some hack and worked on Kubernetes + Istio

@property
    def specs_url(self):
        '''
        The Swagger specifications absolute url (ie. `swagger.json`)

        :rtype: str
        '''
        return url_for(self.endpoint('specs'), _external=False)

I add # 223

and this and worked:

I've found a little bit "hacky" work around. To fix this issue you need to derive from Api class and override _register_apidoc method which will register your custom apidoc.Apidoc definition.

With below snippet, SwaggerUI is served under /api/service/v1 path:

class MyCustomApi(Api):
    def _register_apidoc(self, app: Flask) -> None:
        conf = app.extensions.setdefault('restplus', {})
        custom_apidoc = apidoc.Apidoc('restplus_doc', 'flask_restplus.apidoc',
                                      template_folder='templates', static_folder='static',
                                      static_url_path='/api/service/v1')

        @custom_apidoc.add_app_template_global
        def swagger_static(filename: str) -> str:
            return url_for('restplus_doc.static', filename=filename)

        if not conf.get('apidoc_registered', False):
            app.register_blueprint(custom_apidoc)
        conf['apidoc_registered'] = True


blueprint = Blueprint('my_blueprint', __name__, url_prefix='/api/service/v1')
api = MyCustomApi(blueprint, version='1.0', title='My Custom API', description='My Custom API.',
                  validate=True)

@orkenstein
Copy link

orkenstein commented Oct 29, 2019

I did some hack and worked on Kubernetes + Istio

@property
    def specs_url(self):
        '''
        The Swagger specifications absolute url (ie. `swagger.json`)

        :rtype: str
        '''
        return url_for(self.endpoint('specs'), _external=False)

I add # 223

and this and worked:

I've found a little bit "hacky" work around. To fix this issue you need to derive from Api class and override _register_apidoc method which will register your custom apidoc.Apidoc definition.
With below snippet, SwaggerUI is served under /api/service/v1 path:

class MyCustomApi(Api):
    def _register_apidoc(self, app: Flask) -> None:
        conf = app.extensions.setdefault('restplus', {})
        custom_apidoc = apidoc.Apidoc('restplus_doc', 'flask_restplus.apidoc',
                                      template_folder='templates', static_folder='static',
                                      static_url_path='/api/service/v1')

        @custom_apidoc.add_app_template_global
        def swagger_static(filename: str) -> str:
            return url_for('restplus_doc.static', filename=filename)

        if not conf.get('apidoc_registered', False):
            app.register_blueprint(custom_apidoc)
        conf['apidoc_registered'] = True


blueprint = Blueprint('my_blueprint', __name__, url_prefix='/api/service/v1')
api = MyCustomApi(blueprint, version='1.0', title='My Custom API', description='My Custom API.',
                  validate=True)

Gives me 500 Server Error

@jlongo-encora
Copy link

Any updates on this? I'm having the same issue

@amacd31
Copy link

amacd31 commented Dec 11, 2019

It seems to me that flask_restplus is lacking a mechanism to correctly set the url_prefix when registering the apidoc blueprint here: https://github.com/noirbizarre/flask-restplus/blob/master/flask_restplus/api.py#L243.

A workaround, that appears to work for my current use case at least, is to monkey patch flask_restplus.apidoc.apidoc by setting the required url_prefix directly on the module level object here: https://github.com/noirbizarre/flask-restplus/blob/master/flask_restplus/apidoc.py#L21.

The below snippet will serve the documentation at /api/doc and the swagger UI resources at /api/swaggerui:

from flask import Flask, Blueprint
from flask_restplus import Api

# Import apidoc for monkey patching
from flask_restplus.apidoc import apidoc

URL_PREFIX = '/api'

# Make a global change setting the URL prefix for the swaggerui at the module level
apidoc.url_prefix = URL_PREFIX

app = Flask(__name__)
blueprint = Blueprint('api', 'blueprint_name', url_prefix=URL_PREFIX)
api = Api(blueprint, doc='/doc/')
app.register_blueprint(blueprint)

@gni
Copy link

gni commented Dec 12, 2019

It seems to me that flask_restplus is lacking a mechanism to correctly set the url_prefix when registering the apidoc blueprint here: https://github.com/noirbizarre/flask-restplus/blob/master/flask_restplus/api.py#L243.

A workaround, that appears to work for my current use case at least, is to monkey patch flask_restplus.apidoc.apidoc by setting the required url_prefix directly on the module level object here: https://github.com/noirbizarre/flask-restplus/blob/master/flask_restplus/apidoc.py#L21.

The below snippet will serve the documentation at /api/doc and the swagger UI resources at /api/swaggerui:

from flask import Flask, Blueprint
from flask_restplus import Api

# Import apidoc for monkey patching
from flask_restplus.apidoc import apidoc

URL_PREFIX = '/api'

# Make a global change setting the URL prefix for the swaggerui at the module level
apidoc.url_prefix = URL_PREFIX

app = Flask(__name__)
blueprint = Blueprint('api', 'blueprint_name', url_prefix=URL_PREFIX)
api = Api(blueprint, doc='/doc/')
app.register_blueprint(blueprint)

This one is working for fix the url path to swagger.json and swaggerui folder.
Thanks @amacd31
For make it work behind 1 or 2 proxy's with HTTPS is another story.

@austinjp
Copy link

I've made a "minimal" (kinda) gist here to try to collate some of the approaches listed here, all into one place. I've tried to cover:

  • custom url_prefix
  • serving over HTTPS
  • logging

https://gist.github.com/austinjp/c0c3ed361fd54ed4faf0065eb40502eb

Does it fail for anyone?

@ricardoaat
Copy link

I've used @austinjp gist but had to add the not-so-pretty hack @giovanniattina found and it finally worked in a k8s deployment served throughout HTTPS, here's an extract from my test code:
Note: you might want to lose the CORS decorator and replace the CONTEXT_PATH configuration with your own (probably better) way to get it.

csrf_protect = CSRFProtect()


def _register_apidoc(self, app: Flask) -> None:
    conf = app.extensions.setdefault('restplus', {})
    custom_apidoc = apidoc.Apidoc('restplus_doc', 'flask_restplus.apidoc',
                                  template_folder='templates', static_folder='static',
                                  static_url_path="{context_path}/api/v1".format(context_path=settings.CONTEXT_PATH))

    @custom_apidoc.add_app_template_global
    def swagger_static(filename: str) -> str:
        return url_for('restplus_doc.static', filename=filename)

    if not conf.get('apidoc_registered', False):
        app.register_blueprint(custom_apidoc)
    conf['apidoc_registered'] = True

def api_patches(api_blueprint):

    Api._register_apidoc = _register_apidoc

    @property
    def fix_specs_url(self):
        return url_for(self.endpoint('specs'), _external=False)
    Api.specs_url = fix_specs_url

    api_fixed = Api(
        api_blueprint,
        title="le title",
        description="le description",
        version="0.1.0", doc="/docs", decorators=[csrf_protect.exempt])

    return api_fixed


api_blueprint = Blueprint('api', __name__,
                              url_prefix="{context_path}/api/v1".format(context_path=settings.CONTEXT_PATH))
api = api_patches(api_blueprint)

Hope this help someone not to expend too much time on this like me.

@austinjp
Copy link

austinjp commented Jan 27, 2020

For others who land here via Google or what have you, check this:

#770

jslmariano added a commit to jslmariano/remote_ph_exam_python that referenced this issue May 14, 2020
* Fixed api url behind proxy REF: noirbizarre/flask-restplus#517 (comment)
* Added new module yelp
jslmariano added a commit to jslmariano/yelp_twist that referenced this issue May 14, 2020
* Fixed api url behind proxy REF: noirbizarre/flask-restplus#517 (comment)
* Added new module yelp
@steffen-webb
Copy link

steffen-webb commented Apr 13, 2021

in my setup with load balancer and url rewrite www.domain.com/api/ ---> www.domain.com:8443/
api endpoints with namespaces. I solved this with.:

### This fix, will insert <domain>/api/<url for swagger doc path>
@property
def fix_specs_url(self):
    return "/api/" + url_for(self.endpoint('specs'), _external=False)

### This fix, will insert <domain>/api/<url for swagger base endpoint path>
@property
def fix_base_url(self):
    return "/api" + url_for(self.endpoint('root'), _external=False)
Api.specs_url = fix_specs_url
Api.base_path = fix_base_url

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests