flask-restplus icon indicating copy to clipboard operation
flask-restplus copied to clipboard

Swagger UI refers to http://localhost:5000/swagger.json

Open rndrestore opened this issue 7 years ago • 26 comments

We deploy flask-restplus behind a proxying firewall.

Problem here is that the swagger.html generates this:

url: "http://127.0.0.1:5000/swagger.json"

This is fine for local deployments, but blocked when proxied through a firewall.

The problem come from api.py:

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

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

Using _external=False results in '/swagger.json' and solves this issue, and nicely serves swagger UI via the proxying firewall.

rndrestore avatar Dec 30 '16 14:12 rndrestore

I think your issue is Flask related. Have a look at #132.

ziirish avatar Jan 04 '17 14:01 ziirish

I have the same issue, I've spent the whole day debugging it and hopefully got somewhere, my case:

I have two flask-restplus apps running, one in localhost:5000 and other in localhost:5100. I am using the example on the README.md for both of them. I am deploying them behind nginx.

My intention is that localhost/contacts proxies to localhost:5000 and localhost/notifications proxies to localhost:5100.

With the below changes, things work almost as expected:

  • When accessing localhost/contacts/todos/ it's all good;
  • When accessing localhost/contacts/todos I get redirect to localhost/contacts/todos/ thanks to the proxy_redirect line;
  • When accessing localhost/contacts it fails to load swagger ui. It tries to read swagger.json from localhost/swagger.json instead of localhost/contacts/swagger.json. I can't for the life of me make it work with nginx changes only.
server {

     listen 80;
     server_name localhost;

     location /contacts/ {
         proxy_pass http://localhost:5000/;
         proxy_redirect http://localhost http://localhost/contacts;

         proxy_set_header   Host             $host;
         proxy_set_header   X-Real-IP        $remote_addr;
         proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
         proxy_set_header   X-Forwarded-Proto    $scheme;
         proxy_set_header   Referer $http_referer;
     }
}

The solution was to follow http://flask.pocoo.org/snippets/35/ and add the following to nginx block:

    proxy_set_header   X-Scheme $scheme;
    proxy_set_header   X-Script-Name /contacts/;

And then call flask-restplus with

class ReverseProxied(object):
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
        if script_name:
            environ['SCRIPT_NAME'] = script_name
            path_info = environ['PATH_INFO']
            if path_info.startswith(script_name):
                environ['PATH_INFO'] = path_info[len(script_name):]

        scheme = environ.get('HTTP_X_SCHEME', '')
        if scheme:
            environ['wsgi.url_scheme'] = scheme
        return self.app(environ, start_response)

app = Flask(__name__)
app.wsgi_app = ReverseProxied(app.wsgi_app)
api = Api(app, version='1.0', title='TodoMVC API',
    description='A simple Notification API',
)

Needless to say I think it's an awful solution because something that should be solved in the web server layer has to be solved in a middleware / application layer. What if I don't have access to the flask-restplus server code?

If swagger.json worked with nginx changes only life would be good. I think what I am trying to achieve is a very common case. Any thoughts @ziirish and @noirbizarre ? And thanks for the work on flask-restplus, it's a very good piece of software :)

andrecp avatar Jan 07 '17 06:01 andrecp

I did the worst fix ever but hopefully it can spark a better implementation.

I've created a flask config called SWAGGER_BASEPATH which basically is concatenated to generate the swagger.json URL.

So in my case:

app = Flask(__name__)
app.config['SWAGGER_BASEPATH'] = '/contacts'
api = Api(app, version='1.0', title='TodoMVC API',
    description='A simple Contacts API',
)

And then everything works like a charm without needing to use ReverseProxied, CORS or anything else.

My small implementations (happy to adapt or if the thing already existed please point me to it):

in flask_restplus/api.py:

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

        :rtype: str
        '''
        url = url_for(self.endpoint('specs'), _external=True)
        if self.app.config.get('SWAGGER_BASEPATH', ''):
            prefix = url.split('/swagger.json')[0]
            url = prefix + self.app.config.get('SWAGGER_BASEPATH', '') + '/swagger.json'
        return url

in flask_restplus/swagger.py:

       basepath = current_app.config.get('SWAGGER_BASEPATH', '')
        if not basepath:
            basepath = self.api.base_path
            if len(basepath) > 1 and basepath.endswith('/'):
                basepath = basepath[:-1]

And that's it.

andrecp avatar Jan 07 '17 08:01 andrecp

I don't understand why you want to patch a framework when this should be handled by your application code.

In this case, why won't you patch Flask directly?

@rndrestore issue is typically handled by the ProxyFix thing (the Flask documentation gives you the answer which is IMO the good one).

Your issue is a bit different because you host your application behind a sub-root path. Again, the snippet you mentioned is probably the best answer you can get.

Again, I don't understand why you want to patch the framework. The issue is related to your own environment. Most people won't care about this use-case. Besides, you can (and must) handle this at a different layer in your application stack.

For the record, here is how I handle your use-case:

app.py ReverseProxied config

This way you can either configure your reverse-proxy to send the appropriate header, or you can add a new option to your application.

And I confirm it works with Flask-Restplus since my application heavily relies on this framework.

ziirish avatar Jan 07 '17 09:01 ziirish

I have played with what you mentioned @ziirish , the problem is that I have to duplicate the code (or add an extra lib) / call a reverse proxy on each micro service using flask restplus.

It's also doing things in the middleware layer (slow - python) instead of nginx (fast).

It's a framework that gives a swagger doc (which is amazing) but IMO it has to be configurable to work under reverse proxies without needing to change the application code or add boilerplate to every middleware running a flask-restplus application.

e.g: say I have 500 microservices, with the configuration I can get that path from an environment variable and have it set with Ansible and be done at deployment.

Without it I need to always remember to add a ReverseProxy when using flask restplus, add it in 500 places and make 500 services run slower.

In the end, in my opinion, looks like an immense overkill to add a whole WSGI middlware just to be able to access the auto generated docs, everything else works perfectly fine just with NGINX and no changes in the application layer.

andrecp avatar Jan 07 '17 09:01 andrecp

I understand your situation, but I'm pretty sure Flask-Restplus is not the right place for your patch.

Because this is something related to your own environment, I think it might be a good idea to write your own framework around Flask and Flask-Restplus which implements your fixes.

That's how we always did at my past and current jobs.

ziirish avatar Jan 07 '17 10:01 ziirish

@ziirish having said that it wouldn't hurt to have some reverse proxy examples in the repo for apache/nginx/haproxy that are verified working with restplus.

styk-tv avatar Aug 17 '17 11:08 styk-tv

If users are repeatedly addressing similar issues, these should at minimum be added to the docs, even if they are deemed beyond the scope of a product. Otherwise every adopter is left to cobble a snowflake solution with no benefit from those that have addressed this. @ziirish expresses this implicitly:

"good idea to write your own framework around Flask and Flask-Restplus which implements your fixes. That's how we always did at my past and current jobs."

Schedules don't allow for that. It'll remain proprietary code which everyone needs to maintain in isolation.

I also find the docs here to stop short of the ideal which, IMO, should include some examples that do go beyond the hard line that defines the projects edge boundaries.

bedge avatar Aug 17 '17 16:08 bedge

I think I found some way instead of modifying the framework. I try to derived custom API from flask_restplus.API, and then change _external from True to False, modify the result of specs_url to relative url. that seems work for me.

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

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

Wish good luck!

goodloop avatar Apr 15 '18 21:04 goodloop

@goodloop 's solution works (at least for ngrok).

bitfinity avatar Apr 22 '18 05:04 bitfinity

@goodloop - great stuff! Does work!

avloss avatar Dec 11 '18 03:12 avloss

@goodloop it works, great!!

williamsyb avatar Jan 15 '19 07:01 williamsyb

I redirected two nginx location. Everything works.

location /swagger.json { proxy_pass http://content:5000/swagger.json; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; }

location /swaggerui/ {
    proxy_pass <http://content:5000/>swaggerui/;
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;
}

ligan avatar Feb 22 '19 02:02 ligan

I am forking flask-restplus now to make this change to the main code. I plan to add a behind_proxy=True keyword parameter to the Api(...., behind_proxy=True) object.

I need this feature to use an nginx reverse proxy to serve many REST+ APIs inside kubernetes. I think this will be a popular fix.

We deploy flask-restplus behind a proxying firewall.

Problem here is that the swagger.html generates this:

url: "http://127.0.0.1:5000/swagger.json"

This is fine for local deployments, but blocked when proxied through a firewall.

The problem come from api.py:

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

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

Using _external=False results in '/swagger.json' and solves this issue, and nicely serves swagger UI via the proxying firewall.

adroffner avatar Feb 26 '19 15:02 adroffner

Hello,

It's been a long time since this issue has been opened. The least thing we can do is to document the available solutions.

We'll then be discussing how we could improve your experience.

ziirish avatar Mar 26 '19 21:03 ziirish

I think I found some way instead of modifying the framework. I try to derived custom API from flask_restplus.API, and then change _external from True to False, modify the result of specs_url to relative url. that seems work for me.

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

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

Wish good luck! I tried but failed. Is it the only place we should modify? Do we have to do something in nginx conf?

umialpha avatar Jun 11 '19 14:06 umialpha

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

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

Can you reference the rest of the Flask code that you are using to get this to work? I am a little confused what object you pass to the Custom_API class.

Edit: I was able to answer my own question. See here if anyone is confused like I was: https://github.com/noirbizarre/flask-restplus/issues/543

emr-arvig avatar Jul 15 '19 19:07 emr-arvig

I did a solution for this in my project.

Swagger gets the host information from SERVER_NAME env. But if you change the SERVER_NAME to a different name from your server, you'll not able to access it.

So I've created a 'new' env named SWAGGER_HOST and changed the following line to get this new parameter instead SERVER_NAME and working as expected.

https://github.com/noirbizarre/flask-restplus/blob/a7e363a8352efc70c8d160ef9526dc4572733a1e/flask_restplus/swagger.py#L226

lepri avatar Oct 01 '19 22:10 lepri

Is there any reason @goodloop's solution cannot be integrated into Flask-RestPlus? It works locally and behind a proxy. Would you accept a pull-request containing this fix, together with documentation on how to use Flask-RestPlus behind and nginx reverse-proxy with HTTPS?

rob-smallshire avatar Oct 28 '19 10:10 rob-smallshire

To clarify what @goodloop explained:

DOCS_ROUTES = Blueprint('swagger', __name__)


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

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


API = CustomAPI(DOCS_ROUTES, title='My Flask API',
                version='1.0',
                description='My Flask API Project')

It works with flask==1.1.1 and flask_restplus==0.13.0 behind a Nginx proxy with this example config:

server {

  listen 80;
  server_name mydomain.com;
  return 301 https://$host$request_uri;

}

server {

  listen 443 ssl http2;
  server_name mydomain.com;

  [SSL and other config stuff...]

  location ^~ / {

    proxy_pass http://my_app:5000/;
    proxy_redirect     off;

    proxy_set_header   Host                 $host;
    proxy_set_header   X-Real-IP            $remote_addr;
    proxy_set_header   X-Forwarded-For      $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto    $scheme;

    proxy_connect_timeout       600;
    proxy_send_timeout          600;
    proxy_read_timeout          600;
    send_timeout                600;

  }

}

fabioqcorreia avatar Jan 23 '20 23:01 fabioqcorreia

For anyone who drops here, this project has been replaced by flask-restx as mentioned in #770.

fabioqcorreia avatar Feb 04 '20 00:02 fabioqcorreia

How do i import app in blueprints for swagger?

api_service.py

from flasgger import Swagger
from flask import Blueprint
from flask_restx import Api

api_service = Api(Blueprint('api_service', __name__))
swagger = Swagger(app) #??

service.py

from app.service.api_service import api_service

app = Flask(__name__)
app.register_blueprint(api_service, url_prefix='/api')
swagger = Swagger(app)

aakashrshah avatar May 04 '20 01:05 aakashrshah

1 - please open restx questions against flask-restx not flask-restplus

2 - if you're using flask-restx for swagger, you don't need flasgger's Swagger. Not really sure what is trying to be accomplished there. flask-restx (and flask-restplus) generate your swagger/openapi docs for you, and vendor in the swagger UI.

j5awry avatar May 11 '20 17:05 j5awry

You can follow this solution: https://stackoverflow.com/questions/47508257/serving-flask-restplus-on-https-server

  1. Make sure in your chrome developertools->Network tab that whenever you reload the page(in https server) that shows the swagger UI, you get a mixed content error for swagger.json request.
  2. The solution in stackoverflow post solves the issue when deployed on an https server but locally it might give issue. For that you can use the environment variable trick. Set an environment variable on your https server while deploying your app. Check for that environment variable before applying the solution. It might look similar to this:
import os
from flask import url_for
from flask_restplus import Api

app = Flask( __name__)
if os.environ.get('CUSTOM_ENV_VAR'):
    @property
    def specs_url(self):
        return url_for(self.endpoint('specs'), _external=True, _scheme='https')
 
    Api.specs_url = specs_url
api = Api(app)

Now when you run locally this hack won't be applied and swagger.json would be served through http and in your server it would be served via https.

binoyskumar92 avatar Jun 17 '20 18:06 binoyskumar92

location /swaggerui/ { proxy_pass http://content:5000/swaggerui/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; }

This solved the issue I was having. Thank you!

franchyze923 avatar Nov 15 '21 21:11 franchyze923

I don't agree with have to configure my proxy to pass to /swaggerui, what about when I have multiple microservices behind the same proxy? Please considere use the prefix, to set /${prefix}/swaggerui, thank you!

ltmleo avatar Jan 17 '22 14:01 ltmleo