Search code examples
pythonflaskibm-cloud

Forcing use of HTTPS with Flask's before_request()


I'm writing an app for IBM's Bluemix using Python Flask. I want to force inbound requests to be https. This bit of code works:

# We want to redirect the request to use https. X-Forwarded-Proto is only set in Bluemix runtime. 
forwarded_protocol = request.headers.get('X-Forwarded-Proto', None)
if forwarded_protocol is not None:
    if forwarded_protocol == 'http':
        new_url = request.url.replace('http', 'https', 1)
        return redirect(new_url)

Instead of putting it in each of my route definitions, I'd like to do it in one place. Based on Flask documentation, I think what I want is to use before_request().

The Flask documentation states:

The function will be called without any arguments. If the function returns a non-None value, it’s handled as if it was the return value from the view and further request handling is stopped.

I think this implies that if I return None that processing will continue with the route code for the request. So my implementation would look like:

@app.before_request
def force_https():
    # We want to redirect the request to use https. X-Forwarded-Proto is only set in Bluemix runtime. 
    try:
        forwarded_protocol = request.headers.get('X-Forwarded-Proto', None)
        if forwarded_protocol is not None:
            if forwarded_protocol == 'http':
                new_url = request.url.replace('http', 'https', 1)
                return redirect(new_url)
            else:
                return None
        else:
            return None
    except RuntimeError as e:
        return None

It's clear though that either my implementation or understanding are off. I'm not able to ever hand off control after this method runs to the route code. And before_request() also appears to be called as Flask is starting up, before the first request, hence the try/except block. Is my failure a problem with my implementation, understanding of Flask, both?


Solution

  • You can check to see if the request endpoint is one of the view functions to avoid the runtime error. return None is technically equivalent to return, but if you do nothing, functions in python automatically return None. Make sure that you replace "http://" with "https://" and not just "http" because the string "http" can happen anywhere else in the URL. And request.is_secure is perhaps a better way to check if request is secure than checking X-Forwarded-Proto header. Give the following a try:

    @app.before_request
    def force_https():
        if request.endpoint in app.view_functions and not request.is_secure:
            return redirect(request.url.replace('http://', 'https://'))
    

    You can also create your own decorator for redirecting non-secure requests. See for example this snippet: http://flask.pocoo.org/snippets/93/.