noirbizarre / flask-restplus

Fully featured framework for fast, easy and documented API development with Flask
http://flask-restplus.readthedocs.org
Other
2.73k stars 507 forks source link

Swagger UI path not using url_prefix #517

Open blondowski opened 6 years ago

blondowski commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 5 years ago

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 commented 5 years ago

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

asarchami commented 5 years ago

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

jpowie01 commented 5 years ago

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 commented 5 years ago

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

jpowie01 commented 5 years ago

Strange... I tested it today with Nginx as a reverse proxy without any changes in configuration. Maybe this comment (https://github.com/noirbizarre/flask-restplus/issues/223#issuecomment-381439513) will help you somehow?

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

asarchami commented 5 years ago

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 commented 5 years ago

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: https://github.com/noirbizarre/flask-restplus/issues/262#issuecomment-290065151.

asarchami commented 5 years ago

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 commented 5 years ago

I think this one may help you: https://github.com/noirbizarre/flask-restplus/issues/223#issuecomment-381439513. But I'm not sure about it... 🤔

asarchami commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

Any updates on this?

Kampe commented 5 years ago

Has this been prioritized, the workaround is not ideal

giovanniattina commented 5 years ago

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

giovanniattina commented 5 years ago

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 commented 5 years ago

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 commented 4 years ago

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

amacd31 commented 4 years ago

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)
ioluc commented 4 years ago

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 commented 4 years ago

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:

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

Does it fail for anyone?

ricardoaat commented 4 years ago

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 commented 4 years ago

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

https://github.com/noirbizarre/flask-restplus/issues/770

steffen-webb commented 3 years ago

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