GuidesPYTHON
Fix CORS errors in Flask
Browsers withhold HTTP responses from a frontend application when the target Flask application does not return matching Access-Control headers. When you own the backend service, resolving flask cors issues requires configuring the server to send these headers. The Flask-CORS extension manages origins, preflight requests, and credentials from one place.
Confirm the fix belongs in Flask
A missing Access-Control-Allow-Origin header on responses from your own API is a server-side configuration issue, not a problem with the client-side fetch call. Consult the diagnosis guide to verify whether the failing request points to an endpoint you control.
If the failing request targets a third-party API that you cannot modify, server configuration is not an option. A public proxy like cors.dev can re-serve public GET and HEAD responses from public HTTPS hosts with the headers your origin needs. The proxy operator can observe everything routed through it, so route public data only.
Install Flask-CORS
The Flask-CORS extension is the canonical package for this. Install it with pip; the package imports as flask_cors.
$ pip install -U flask-cors
# The PyPI package flask-cors imports as flask_corsEnable CORS on the application
Import CORS and pass the Flask application instance to initialize the extension.
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # every route, every origin: fine for development
@app.route("/api/users")
def list_users():
return {"users": ["ada", "grace"]}By default, this permits cross-origin requests on all routes, for all origins, and allows GET, HEAD, POST, OPTIONS, PUT, PATCH, and DELETE methods. The allow_headers setting defaults to *. Because origins defaults to * and send_wildcard defaults to False, the extension echoes the incoming Origin value back in the Access-Control-Allow-Origin header rather than returning a literal asterisk.
Restrict origins before production
Scope allowed domains using the resources dictionary on the extension, or apply the @cross_origin() decorator to individual routes.
from flask import Flask
from flask_cors import CORS, cross_origin
app = Flask(__name__)
CORS(app, resources={
r"/api/*": {"origins": ["https://app.example.com"]},
})
# Per-route alternative when the extension is not initialized
@app.route("/public/status")
@cross_origin(origins=["https://status.example.com"])
def public_status():
return {"ok": True}- Origin strings must include the URL scheme, and the port number whenever it differs from the protocol default, such as
http://localhost:8000orhttps://app.example.com. - Keys in the
resourcesdictionary are regular expressions matched against the request path. When multiple patterns match a route, the longest regex takes precedence. - As an alternative to global application configuration, apply
@cross_origin()directly under@app.route(...)to configure rules on a single view. The decorator accepts the same options as the extension.
How the preflight is answered
Flask automatically generates an OPTIONS response for every registered route, and Flask-CORS attaches the necessary access control headers to it. You do not need to write a custom OPTIONS route handler, and the route never has to know the probe happened.
- Browsers never send user credentials or cookies with preflight OPTIONS requests.
- The
max_agesetting accepts an integer in seconds or atimedeltato set theAccess-Control-Max-Ageheader, which is not sent by default. - The
@cross_origin()decorator setsautomatic_optionstoTrueby default, so decorated routes answer OPTIONS requests with CORS headers. - Setting
logging.getLogger('flask_cors').level = logging.DEBUGmakes the extension log its decisions to the application logs.
Set the headers by hand instead
You can append access control headers manually across all outgoing responses using an application after-request hook.
from flask import Flask, request
app = Flask(__name__)
ALLOWED_ORIGINS = {"https://app.example.com"}
@app.after_request
def add_cors_headers(response):
origin = request.headers.get("Origin")
if origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Vary"] = "Origin"
return response
# You still owe the preflight: Flask's automatic OPTIONS response
# does not add Access-Control-Allow-Methods or -Allow-Headers here.The extension is the better call once any of preflight answers, per-resource rules, or origin matching matter: an @app.after_request hook re-implements the CORS specification by hand, and every gap in that re-implementation surfaces as a browser block.
Allow cookies and Authorization headers
When a request transmits credentials such as cookies or HTTP authentication headers, the browser requires an explicit origin match. Browsers reject any response carrying Access-Control-Allow-Origin: * when the client request sets credentials: 'include'.
- Setting
supports_credentials=Trueinstructs the extension to returnAccess-Control-Allow-Credentials: true. - Per the Flask-CORS documentation,
supports_credentials=Truecannot be combined with a literal*origin. - The Flask-CORS documentation recommends adding CSRF protection to your application before allowing cross-site cookies.
- The client fetch invocation must explicitly declare
credentials: 'include'for the browser to attach stored cookies to cross-origin requests.
Verify the headers with curl
Test your endpoints from the terminal using curl to simulate standard and preflight requests with an Origin header.
$ curl -i -H "Origin: https://app.example.com" \
http://127.0.0.1:5000/api/users
HTTP/1.1 200 OK
access-control-allow-origin: https://app.example.com
# Simulate the preflight the browser sends before a JSON POST
$ curl -i -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type" \
http://127.0.0.1:5000/api/users
HTTP/1.1 200 OK
access-control-allow-methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT
access-control-allow-headers: content-typeA passing configuration returns Access-Control-Allow-Origin matching your request origin on standard GET requests. An OPTIONS preflight request must return HTTP status 200 with an Access-Control-Allow-Methods header that includes the method you intend to call.
Good questions.
Does Flask-CORS handle OPTIONS preflight requests automatically?
Yes. Flask automatically provides an OPTIONS response for every route, and Flask-CORS attaches the Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Allow-Origin headers without requiring a custom OPTIONS view handler.
Why does my API request work in curl or Postman but fail in the browser?
CORS is enforced by web browsers, not by the server or command-line HTTP clients. Tools like curl and Postman do not restrict cross-origin access and ignore missing Access-Control headers, whereas browsers withhold responses from client scripts when valid headers are absent.
Can CORS be enabled for a single route instead of the whole application?
Yes. You can leave the global extension uninitialized and apply the @cross_origin() decorator directly underneath @app.route(...) on specific view functions, passing parameters such as origins, methods, and allow_headers.
Why did supports_credentials fail together with a wildcard origin?
The CORS protocol prohibits combining Access-Control-Allow-Credentials: true with a literal wildcard Access-Control-Allow-Origin: * header. The Flask-CORS documentation states that supports_credentials=True cannot be used in conjunction with a * origin.