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 refers to http://localhost:5000/swagger.json #223

Open
rndrestore opened this issue Dec 30, 2016 · 26 comments
Open

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

rndrestore opened this issue Dec 30, 2016 · 26 comments

Comments

@rndrestore
Copy link

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.

@ziirish
Copy link
Collaborator

ziirish commented Jan 4, 2017

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

@andrecp
Copy link

andrecp commented Jan 7, 2017

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
Copy link

andrecp commented Jan 7, 2017

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 added a commit to andrecp/flask-restplus that referenced this issue Jan 7, 2017
Useful to be able to get swagger URLs from other locations when using reverse a proxy. See I noirbizarre#223
@ziirish
Copy link
Collaborator

ziirish commented Jan 7, 2017

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.

@andrecp
Copy link

andrecp commented Jan 7, 2017

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.

@ziirish
Copy link
Collaborator

ziirish commented Jan 7, 2017

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.

@styk-tv
Copy link

styk-tv commented Aug 17, 2017

@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.

@bedge
Copy link

bedge commented Aug 17, 2017

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.

@goodloop
Copy link

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!

@bitfinity
Copy link

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

@avloss
Copy link

avloss commented Dec 11, 2018

@goodloop - great stuff! Does work!

@williamsyb
Copy link

@goodloop it works, great!!

@ligan
Copy link

ligan commented Feb 22, 2019

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;
}

@adroffner
Copy link

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.

@ziirish
Copy link
Collaborator

ziirish commented Mar 26, 2019

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.

@umialpha
Copy link

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?

@emr-arvig
Copy link

emr-arvig commented Jul 15, 2019

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:
#543

@lepri
Copy link

lepri commented Oct 1, 2019

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.

hostname = current_app.config.get('SERVER_NAME', None) or None

@rob-smallshire
Copy link

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?

@fabioqcorreia
Copy link

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
Copy link

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

@aakashrshah
Copy link

aakashrshah commented May 4, 2020

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)

@j5awry
Copy link
Collaborator

j5awry commented May 11, 2020

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.

@binoyskumar92
Copy link

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.

@franchyze923
Copy link

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!

@ltmleo
Copy link

ltmleo commented Jan 17, 2022

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!

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

No branches or pull requests