jqueryajaxcorsbottle

Bottle Py: Enabling CORS for jQuery AJAX requests


I'm working on a RESTful API of a web service on the Bottle Web Framework and want to access the resources with jQuery AJAX calls.

Using a REST client, the resource interfaces work as intended and properly handle GET, POST, ... requests. But when sending a jQuery AJAX POST request, the resulting OPTIONS preflight request is simply denied as '405: Method not allowed'.

I tried to enable CORS on the Bottle server - as described here: http://bottlepy.org/docs/dev/recipes.html#using-the-hooks-plugin But the after_request hook is never called for the OPTIONS request.

Here is an excerpt of my server:

from bottle import Bottle, run, request, response
import simplejson as json

app = Bottle()

@app.hook('after_request')
def enable_cors():
    print "after_request hook"
    response.headers['Access-Control-Allow-Origin'] = '*'
    response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
    response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'

@app.post('/cors')
def lvambience():
    response.headers['Content-Type'] = 'application/json'
    return "[1]"

[...]

The jQuery AJAX call:

$.ajax({
    type: "POST",
    url: "http://192.168.169.9:8080/cors",
    data: JSON.stringify( data ),
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function(data){
        alert(data);
    },
    failure: function(err) {
        alert(err);
    }
});

The server only logs a 405 error:

192.168.169.3 - - [23/Jun/2013 17:10:53] "OPTIONS /cors HTTP/1.1" 405 741

$.post does work, but not being able to send PUT requests would defeat the purpose of a RESTful service. So how can I allow the OPTIONS preflight request to be handled?


Solution

  • Install a handler instead of a hook.

    There are two complementary ways I've done this in the past: decorator, or Bottle plugin. I'll show you both and you can decide whether one (or both) of them suit your needs. In both cases, the general idea is: a handler intercepts the response before it's sent back to the client, inserts the CORS headers, and then proceeds to return the response.

    Method 1: Install Per-route (Decorator)

    This method is preferable when you only want to run the handler on some of your routes. Just decorate each route that you want it to execute on. Here's an example:

    import bottle
    from bottle import response
    
    # the decorator
    def enable_cors(fn):
        def _enable_cors(*args, **kwargs):
            # set CORS headers
            response.headers['Access-Control-Allow-Origin'] = '*'
            response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
            response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
    
            if bottle.request.method != 'OPTIONS':
                # actual request; reply with the actual response
                return fn(*args, **kwargs)
    
        return _enable_cors
    
    
    app = bottle.app()
    
    @app.route('/cors', method=['OPTIONS', 'GET'])
    @enable_cors
    def lvambience():
        response.headers['Content-type'] = 'application/json'
        return '[1]'
    
    app.run(port=8001)
    

    Method 2: Install Globally (Bottle Plugin)

    This method is preferable if you want the handler to execute on all or most of your routes. You'll just define a Bottle plugin once, and Bottle will automatically call it for you on every route; no need to specify a decorator on each one. (Note that you can use a route's skip parameter to avoid this handler on a per-route basis.) Here's an example that corresponds to the one above:

    import bottle
    from bottle import response
    
    class EnableCors(object):
        name = 'enable_cors'
        api = 2
    
        def apply(self, fn, context):
            def _enable_cors(*args, **kwargs):
                # set CORS headers
                response.headers['Access-Control-Allow-Origin'] = '*'
                response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
                response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
    
                if bottle.request.method != 'OPTIONS':
                    # actual request; reply with the actual response
                    return fn(*args, **kwargs)
    
            return _enable_cors
    
    
    app = bottle.app()
    
    @app.route('/cors', method=['OPTIONS', 'GET'])
    def lvambience():
        response.headers['Content-type'] = 'application/json'
        return '[1]'
    
    app.install(EnableCors())
    
    app.run(port=8001)