Cross-domain HTTP with Python

For security and other reasons browsers put limitations on what sites you can access from the Javascript. The basic rule is: if the web page containing the script originates form mysite.com, then the script is only allowed to access mysite.com.

New Firefox and Chrome versions allow the server owner to bypass these limitations. When Javascript tries to make a cross-domain (or cross-site, whatever you call it) request, the browser does not outright deny it. Instead the browser will reach to the server and ask “I would like to make this kind of request, is it allowed?”. The browser is using the OPTIONS method to place this question. If server gives the permission, browser sends the actual request.

This is all documented for example in Mozilla Developer Network so I’m not going to duplicate the instructions for setting appropriate headers here.

One thing that might not be obvious is that at least Firefox seems to require that you also set these Access-Control headers for the actual response. Remember, the browser is making two requests. First with method OPTIONS, then with GET or POST. As Carsten has also noticed, Firefox may not give the actual response to Javascript, if the server does not set the access-control headers also to the response. There is also a Stackoverflow question that partly covers this.

Now, fast forward to the Python part. Below is a simple Python decorator that can be used for example with Django to allow the cross-domain requests to your application. Before you use this for anything real, take a look at the access-control headers it is setting and compare those with the documentation.

The decorator is meant to be used on your view function that takes a request in and returns a response. For the OPTIONS method the decorator does not call the actual function at all. Instead it just sets the access control headers to the response and returns. For the actual GET/POST the decorator first calls the function and then adds the access-control headers.

@HttpOptionsDecorator
def retrieve_rate(request, currency):
    # do something
    return HttpResponse(....)
def set_access_control_headers(response):
    response['Access-Control-Allow-Origin'] = '*'
    response['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
    response['Access-Control-Max-Age'] = 1000
    response['Access-Control-Allow-Headers'] = '*'
    
class HttpOptionsDecorator(object):
    def __init__(self, f):
        self.f = f;
    
    def __call__(self, *args):
        logging.info("Call decorator")
        request = args[0]
        if request.method == "OPTIONS": 
            response = HttpResponse()        
            set_access_control_headers(response)            
            return response        
        else:
            response = self.f(*args)
            set_access_control_headers(response)
            return response