search by tags

for the user

adventures into the land of the command line

adding CORS for flask, nginx and ajax

Ok so this is the setup:

An ajax request from the browser at a.domain.com sends a DELETE request to an api on a server at b.domain.com.

a.domain.com is a flask webapp running in gunicorn, with an nginx reverse proxy.

b.domain.com is a flask webapp also running in gunicorn, also with an nginx reverse proxy.

a.domain.com and b.domain.com are on separate servers.

The purpose of the ajax request in this example is to delete a transaction on a page, and then update the UI to remove the div that the deleted transaction was located. It looks like this:

$( ".some-button" ).click(function( event ) {
                    event.preventDefault();
                    var payload = {'transaction_id':'some_id'};
                    var that = $(this);

                    $.ajax({
                        type : "DELETE",
                        crossDomain: true,
                        dataType: 'json',
                        url : completeUrl,
                        data: JSON.stringify(payload, null, '\t'),
                        contentType: 'application/json'
                    }).done( function(result) {
                        that.parents('div.transaction').animate({
                            "height" : "0",
                            "opacity" : "0",
                            }, {
                            duration: 400,
                            complete : function() {
                                $(this).parents('div.transaction').remove();
                            }
                        });
                    });
                });

Ajax will by default send a ‘preflight’ HTTP OPTION request to b.domain.com, asking what it is allowed to do, as it is at a different domain.

You need to set up the server at b.domain.com to respond to this preflight request with what you want to allow a.domain.com to do.

I do this in my nginx config:

server {
.
.
    location / {
    if ($request_method = 'OPTIONS') {
            add_header          'Access-Control-Allow-Origin' 'https://a.domain.com';
            add_header          'Access-Control-Allow-Methods' 'DELETE';
            add_header          'Access-Control-Allow-Headers' 'Content-Type';
            add_header          'Access-Control-Allow-Credentials' 'true';
            add_header          'Access-Control-Max-Age' 60;
            add_header          'Content-Type' 'text/plain charset=UTF-8';
            add_header          'Content-Length' 0;
            return 204;
        }
        .
        .
}

Nginx will return these access control headers to a.domain.com. You gotta make sure you have Access-Control-Allow-Origin, it’s super important.

a.domain.com will see response headers that include these:

access-control-allow-credentials:true
access-control-allow-headers:Content-Type
access-control-allow-methods:DELETE
access-control-allow-origin:https://a.domain.com
access-control-max-age:60

Cool, now a.domain.com knows what it’s allowed to do, and will continue with the actual DELETE request. b.domain.com’s nginx will forward this request to its flask app. Flask will do whatever and then return a response. The response will also need to include the OPTIONS directive and these headers:

@app.route('/v1/blah' , methods=['GET','POST','PATCH','DELETE','OPTIONS'])
.
.
.
response['token'] = token
js = json.dumps(response)
resp = Response(js, status=201, mimetype='application/json')
resp.headers['Access-Control-Allow-Origin'] = a.domain.com
resp.headers['Access-Control-Allow-Methods'] = 'DELETE'
resp.headers['Access-Control-Allow-Headers'] = 'Content-Type'
resp.headers['Access-Control-Allow-Credentials'] = 'true'
resp.headers['Access-Control-Max-Age'] = 60
return resp

It’s really important that the Access-Control-Allow-Origin response header contains the domain for a.domain.com; in other words, the domain you want to allow access to b.domain.com. It can also be ’*’, which is super unsafe, or match exactly. You can’t wildcard it like *.domain.com. That won’t work.

If you do not have these headers, especially the Access-Control-Allow-Origin header, you will see this error:

XMLHttpRequest cannot load https://b.domain.com. No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'a.domain.com' is therefore not allowed access.

Also this can only be tested when using a real domain name, 127.0.0.1 will result in this error:

OPTIONS https://127.0.0.1/v1/blah?token=blah net::ERR_INSECURE_RESPONSE

Unless you do some hacky stuff like alias your localhost’s DNS in /etc/hosts or restart chrome with a special switch to not check certificates or just not use SSL. Testing this stuff can be tricky.