Enabling CORS support for server

tutorial python bottlepy cors backend

so i am working on this project

Project brief: there is this webapp that I am working on right now. It consist of a single-page application (SPA) serving the user interface, connected to a data API serving the content.

On my developement environment, I am running the two services on different ports (the SPA on port 8000, the API on port 8080).

On the SPA, we wrote an API object that will make XHR requests to the content server. In theory, this should work just fine. The page send a request to the server then load the response into the interface, what is so hard about it?

This is what is so hard about it.

going into CORS

CORS, cross orgin request sharing. All I knew was that browsers are careful when different hosts share resources because of cross-site scripting. Beyond that, I am clueless as to how the whole CORS process works and how to implement it on a server.

Looking at the error message, it seems like I am missing a Access-Control-Allow-Origin header. So I just added it in, in hope that this simple solution will do. The sample below is a very simplified version of what I did.

I am using bottle.py here, but the idea is the similar for whatever framework you use (flask, web.py, webapp2, etc)

from bottle import route, app

@route('/')
def index():
    bottle.response.set_header("Acess-Control-Allow-Origin", "*")
    return 'ok'

if __name__=='__main':
    app.run()

it doesn’t work?!?!

I tried accessing the resource from the terminal: curl -i localhost:8080. Seems to return the right value. Should work by now right?

error again

didn’t i send out a get request?

500 Internal Error? My server doesn’t accept OPTIONS request? Why does my GET request keeps changing into an OPTIONS request? What keeps changing the XHR’s method?

I went about searching and rewriting my XHR function, but there doesn’t seem to be anything wrong. There shouldn’t be an reason why the method is changed. So, what’s up with the OPTIONS request?

As I found out after hours of Googling later, the OPTIONS request is part of the CORS checking process. What else does this CORS include exactly? I decided I should read up more on the protocol before continuing.

CORS basics

This is what I learnt. The CORS protocol verifies if a request is legitimate. And how does the browser check the legitimacy of a request? By sending pre-flight OPTIONS request to check.

The OPTIONS request is kind of like the browser asking the server: “So… what kind of requests do you accept”. And if the response value fits the XHR request, the request is sent and everything goes well, else, you will trigger the Cross-Origin Request Blocked error.

The XHR request will be matched with the headers returned in response to the OPTIONS request. These headers will describe what kind of requests this server is allowed to respond to (the requests’ methods, their origin, etc)

so let’s set it up

First of all, we will need to allow our server to accept OPTIONS request; no use having the framework drop all your requests.

We will then add in all the appropriate headers that CORS requires, which includes Access-Control-Allow-Origin, 'Access-Control-Allow-Methods and Access-Control-Allow-Headers. (there are more of course, but these are sufficient)

So what does the 3 headers represent? The names are pretty intuitive.

Access-Control-Allow-Origin will determine if your request origin (host of the frontend SPA) is on the whitelist of the server. To allow access from all pages,set this value to *.

The second header, Access-Control-Allow-Methods determines what kind of methods are allowed. These will include the usual GET, POST methods. Your list should include OPTIONS too. (Since the browser would have to make a request to know what kind of requests are valid)

Access-Control-Allow-Headers is a list of headers that your requests are allowed to attached (Origin is one that is used in CORS) on the request going to the server.

this is how you do it

Putting all of that into code

from bottle import route, app

@route('/', methods='GET OPTIONS'.split())
def index():
    bottle.response.set_header("Access-Control-Allow-Origin", "*")
    bottle.response.set_header("Access-Control-Allow-Origin", "GET, POST, OPTIONS")
    bottle.response.set_header("Access-Control-Allow-Headers", "Origin, Content-Type")

    return 'ok'

if __name__=='__main':
    app.run()

I tested it out and tried the reach the API server through my frontend API. It works!!!

a better way to do this

This code here stinks, there has to be a much better way of writing this…

I took out the headers and place them in a decorator. Since we will be using it all the time, makes no sense to have to write them over and over again.

from bottle import route, app

def cors(func):
    def wrapper(*args, **kwargs):
        bottle.response.set_header("Access-Control-Allow-Origin", "*")
        bottle.response.set_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
        bottle.response.set_header("Access-Control-Allow-Headers", "Origin, Content-Type")
        
        # skip the function if it is not needed
        if bottle.request.method == 'OPTIONS':
            return

        return func(*arg, **kwargs)
    return wrapper

@route('/', methods='GET OPTIONS'.split())
@cors
def index():
    return 'ok'

if __name__=='__main':
    app.run()

that’s all?

Yes, that is all to setting up your python API server to handle CORS pre-flight requests. Now your API server will work as you expect it to.

The idea here is transferable to different frameworks, so as long as you know how to set the response headers you are good to go.

On the frontend side, all you have to do is to create a XHR that opens to your API endpoint, nothing fancy. If you need some help, here is some javascript to get you started.

function fetch(url){
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.addEventListener("load", function(){
        console.log(this.responseText);
    });
    xhr.send();

    return xhr;
}

fin

If you have any questions, or some part of it doesn’t seem to work, just leave a comment below, and let me see if I can help you with it.

Keep coding :)