Bottle Py: Enabling CORS for jQuery AJAX requests

22,951

Solution 1

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)

Solution 2

Here's a minor improvement on @ron.rothman's method #2 for installing the CORS handler globally. His method requires you to specify that the OPTIONS method is accepted on every route you declare. This solution installs a global handler for all OPTIONS requests.

@bottle.route('/<:re:.*>', method='OPTIONS')
def enable_cors_generic_route():
    """
    This route takes priority over all others. So any request with an OPTIONS
    method will be handled by this function.

    See: https://github.com/bottlepy/bottle/issues/402

    NOTE: This means we won't 404 any invalid path that is an OPTIONS request.
    """
    add_cors_headers()

@bottle.hook('after_request')
def enable_cors_after_request_hook():
    """
    This executes after every route. We use it to attach CORS headers when
    applicable.
    """
    add_cors_headers()

def add_cors_headers():
    if SOME_CONDITION:  # You don't have to gate this
        bottle.response.headers['Access-Control-Allow-Origin'] = '*'
        bottle.response.headers['Access-Control-Allow-Methods'] = \
            'GET, POST, PUT, OPTIONS'
        bottle.response.headers['Access-Control-Allow-Headers'] = \
            'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'

```

Solution 3

Also shouldn't you actually be using this?

response.set_header('Access-Control-Allow-Origin', '*')
response.add_header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS')

Solution 4

Consider letting your webserver, not Bottle, set the headers.

Not sure whether this applies in your situation, but I've solved the problem in past projects by setting CORS headers for my Bottle application in Apache. It's easy to configure, keeps my Python code nice and clean, and is efficient.

Information is available from many sources, but if you're using Apache, here's what my config looks like (more or less):

<Location "/cors">
    Header set Access-Control-Allow-Headers "Origin, Content-Type"
    Header set Access-Control-Allow-Methods "POST, GET, OPTIONS"
    Header set Access-Control-Allow-Origin "*"
    Header set Access-Control-Request-Headers "Origin, Content-Type"
</Location>
Share:
22,951

Related videos on Youtube

Joern
Author by

Joern

Updated on February 22, 2020

Comments

  • Joern
    Joern over 4 years

    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?

  • Joern
    Joern about 11 years
    Hi Ron, thanks a lot for your answers. I've tried both your methods but unfortunately neither worked for me. The issue persists that the OPTIONS request gets outright denied with Error 405. Also, is there a significant difference between Method 2 and calling enable_cors via a hook? To me they look virtually the same (though I'm no python nor bottle expert). What seems to work is this cors_plugin ( github.com/myGengo/bottle-cors ). It seems to add a dedicated @options routing in the RequestPreflightPlugin class. This appears to do the trick. Any idea why your code doesn't work?
  • ron rothman
    ron rothman about 11 years
    Oops, sorry, I forgot to include the method param in the route call, to tell Bottle to accept OPTIONS requests. (Otherwise the default method is just GET.) I've updated the code snippets; looks like it's working now; hopefully works for you too.
  • Joern
    Joern about 11 years
    Ok, it works now! But with your solution, I'd need to create a new route just for method OPTIONS for each request I want to allow preflights (i.e. all in my REST API)?It can't be piggybacked by existing routes as the defined methods expect request.json to be either correctly populated or incorrect (i.e. an error).
  • ron rothman
    ron rothman about 11 years
    Cool! Hmm, wouldn't your original hook-based idea have had the same property? (I.e., conflict with methods that expect request.json.) In any case, I think it's solvable; if you want help, I can modify my code, no problem.
  • ron rothman
    ron rothman about 11 years
    Done! No new routes needed. Actually, I'm glad you made that last point, because I think my new code is better than it was originally. So, thanks. :)
  • ron rothman
    ron rothman about 11 years
    Joern, did this do the trick? Let me know if I can be of more help. (And if you see fit to accept the answer as correct, I'd appreciate it!)
  • Joern
    Joern about 11 years
    Hi, sorry for the wait, I'm hardly finding time for programming. Your code is still faulty (apart from the indentation error in Method 2). Only OPTIONS requests are allowed via CORS as the if-check on request method makes sure that for any other method, the response header is not set to ...-Allow-Origin: *. I removed the if-line and put a if bottle.request.method != 'OPTIONS':instead of the else:. Now everything works great :) If you can fix that, I can mark your answer as the solution! You were really helpful thanks.
  • Joern
    Joern about 11 years
    also: the cors_plugin linked in the very first comment doesn't seem to work if the very first request the server receives is preflighted. A non-preflighted request seems to trigger the registration of the OPTIONS method, only then are OPTIONS requests handled. Very awkward.
  • ron rothman
    ron rothman about 11 years
    Sorry, didn't mean to rush you--happy to hear you got it working with that change. I'm not sure I totally understood the fix you suggested--so when you have a minute, take a look at the modifications I made and let me know if that's what you meant.
  • Joern
    Joern about 11 years
    Yup, just like that, although the if block in Method 2 should be 1 indentation level to the right (like in Method 1).
  • ron rothman
    ron rothman about 11 years
    Yikes, thought I fixed the indentation. Should be good now. Thanks for the collaboration!
  • user1071182
    user1071182 almost 9 years
    This worked for me on all browsers except firefox. Chrome and Safari working splendidly, but Firefox won't even execute the request. Anyone have any idea why? No headers show up in "Live HTTP Headers" plugin or the "Network" tab when clicking button in firefox, but everything show up fine in chrome.
  • Checo R
    Checo R about 5 years
    This worked for me with Python 3.6 and bottle 0.12.16. The accepted answer throws and error when trying to write headers
  • eatmeimadanish
    eatmeimadanish about 5 years
    I use async GEVENT pywsgi and an AWS load balancer for mine. I really do not see the point of running apache with the load balancer options available in AWS.
  • ron rothman
    ron rothman about 5 years
    Thanks. OP didn't mention AWS so not sure how that's relevant to this question.
  • 73k05
    73k05 about 4 years
    This method works for a cheroot cherrypy server wrapped in bottle, thanks
  • Giulio
    Giulio almost 4 years
    I am using pypi.org/project/bottle-cors, works ok.
  • user3072843
    user3072843 almost 4 years
    Could you add a route? I don't know how to use this.