Cross-site HTTP requests are HTTP requests for resources from a different domain than the domain of the resource making the request. For instance, a resource loaded from Domain A makes a request for a resource on Domain B. The way this is implemented in modern browsers is by using HTTP Access Control headers: Documentation on developer.mozilla.org.
The following view decorator implements this:
from datetime import timedelta
from flask import make_response, request, current_app
from functools import update_wrapper
def crossdomain(origin=None, methods=None, headers=None,
max_age=21600, attach_to_all=True,
automatic_options=True):
if methods is not None:
methods = ', '.join(sorted(x.upper() for x in methods))
if headers is not None and not isinstance(headers, basestring):
headers = ', '.join(x.upper() for x in headers)
if not isinstance(origin, basestring):
origin = ', '.join(origin)
if isinstance(max_age, timedelta):
max_age = max_age.total_seconds()
def get_methods():
if methods is not None:
return methods
options_resp = current_app.make_default_options_response()
return options_resp.headers['allow']
def decorator(f):
def wrapped_function(*args, **kwargs):
if automatic_options and request.method == 'OPTIONS':
resp = current_app.make_default_options_response()
else:
resp = make_response(f(*args, **kwargs))
if not attach_to_all and request.method != 'OPTIONS':
return resp
h = resp.headers
h['Access-Control-Allow-Origin'] = origin
h['Access-Control-Allow-Methods'] = get_methods()
h['Access-Control-Max-Age'] = str(max_age)
if headers is not None:
h['Access-Control-Allow-Headers'] = headers
return resp
f.provide_automatic_options = False
return update_wrapper(wrapped_function, f)
return decorator
Unmodified this decorator requires Flask 0.8. If you want to use it with earlier versions of Flask you need to make sure to explicitly enable 'OPTIONS' as method when creating the route.
Here a basic overview of what the parameters do:
methods: Optionally a list of methods that are allowed for this view. If not provided it will allow all methods that are implemented.
headers: Optionally a list of headers that are allowed for this request.
origin: '*' to allow all origins, otherwise a string with a URL or a list of URLs that might access the resource.
max_age: The number of seconds as integer or timedelta object for which the preflighted request is valid.
attach_to_all: True if the decorator should add the access control headers to all HTTP methods or False if it should only add them to OPTIONS responses.
automatic_options: If enabled the decorator will use the default Flask OPTIONS response and attach the headers there, otherwise the view function will be called to generate an appropriate response.
And here is how you can use it:
@app.route('/my_service')
@crossdomain(origin='*')
def my_service():
return jsonify(foo='cross domain ftw')
Please keep in mind that with Flask 0.7 you need to explicitly provide OPTIONS:
@app.route('/my_service', methods=['GET', 'OPTIONS'])
@crossdomain(origin='*')
def my_service():
return jsonify(foo='cross domain ftw')
This snippet by Armin Ronacher can be used freely for anything you like. Consider it public domain.
Comments
Small fix by Audrius Kažukauskas on 2012-10-12 @ 12:38
Needed to add "f.required_methods = ['OPTIONS']" after "f.provide_automatic_options = False" line, otherwise my route with methods=['POST'] wasn't working (405 response for OPTIONS request).
Get a HTTP 301 when using GET by Julien Sarazin on 2012-11-02 @ 12:39
Hello,
At first i'd like to thank you for this decorator which resolve exactly my cross-domain requests problems.
I'm new to python technology so i may forgot sg important but I added ur snippets to my python module then i used it like u suggested :
@app.route('/my/endpoint/path') @crossdomain(origin='*') def my_service(): ... return jsonify(jsondata)
But i get this error when i call this service by GET : -OPTIONS /my/endpoint/path HTTP/1.1" 301.
I tried to add the "f.required_methods = ['OPTIONS'] like Audrius mentioned it in his comment but it has no effect..
Info : Flask 0.9 with python 2.7
Thanks in advance for help.
Support for LoginManager? by Jonathan Abbett on 2012-11-29 @ 02:04
Hi--
I'm using LoginManager's @login_required on a route, along with @crossdomain(origin='*'). When the user is not logged in, @login_required returns a 401 Not Authorized response, but it's not including the Allow-Origin header, so the browser rejects the AJAX request altogether.
Any suggestions on how to solve this? Thanks!
Stripe Webhooks by Dakin Sloss on 2013-01-16 @ 03:50
So is this how I handle setting up webhooks for outside services like Stripe payments? Right now I am getting a 301 error and not sure why..., tried to pop this in and that has not fixed the problem. Any suggestions?
Small change for flask-restless by Nico Gevers on 2013-04-10 @ 13:47
Thanks for the decorator and making my life a lot easier.
I needed it to work for my flask-restless endpoints. I changed the decorator slightly to only return the origin headers if the response is a 2xx response (ie ignore 404s and other errors). I simply check the first character of the response status to make sure it starts with a 2.
if not attach_to_all and request.method != 'OPTIONS' or args[0].status[0] != '2': return resp
In my actual app I decorate every response as follows:
@app.after_request @crossdomain(origin="*") def after(response): return response