Fix: Flask CORS Not Working
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix CORS errors in Flask — installing flask-cors correctly, handling preflight OPTIONS requests, configuring origins with credentials, route-specific CORS, and debugging missing headers.
The Error
A browser request to your Flask API fails with a CORS error:
Access to fetch at 'http://localhost:5000/api/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on
the requested resource.Or after installing flask-cors, CORS still doesn’t work for some routes:
Access to XMLHttpRequest at 'http://localhost:5000/api/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: Response to preflight request doesn't pass access control
check: It does not have HTTP ok status.Or with credentials:
The value of the 'Access-Control-Allow-Origin' header in the response must not be
the wildcard '*' when the request's credentials mode is 'include'.Why This Happens
Flask doesn’t add CORS headers by default. The flask-cors extension handles this, but its configuration model is deceptively flexible — origins can be set at the app level, the blueprint level, the route level, or via the resources mapping, and the precedence is not always obvious. A single missing Access-Control-Allow-Origin header is enough for the browser to reject the response, even when the application logic is perfect.
The other half of the problem is the preflight. Modern browsers send an OPTIONS request before any request that uses a custom header (including Content-Type: application/json), uses a non-simple method (PUT, DELETE, PATCH), or carries credentials. If your Flask route is declared with methods=['POST'] and you do not handle OPTIONS, Flask returns 405 Method Not Allowed — and the browser shows a CORS error rather than the 405, hiding the real cause.
Errors on the server are the third common trap. When a view raises an exception, Flask’s default 500 handler builds the response without going through flask-cors’s after_request hook in some versions, so the response is missing the headers. The browser then reports a CORS failure instead of the actual server bug, and you waste hours adjusting CORS settings when the real fix is an unhandled KeyError in the view.
flask-corsnot installed or not initialized —CORS(app)was not called, so no headers are added.CORS(app)called after route definitions — Flask extension registration order matters for some setups.- Preflight OPTIONS not handled — for requests with custom headers or methods like PUT/DELETE, browsers send an OPTIONS request first. If Flask returns 405 for OPTIONS, the actual request is never sent.
- Wildcard origin with credentials —
origins='*'andsupports_credentials=Truetogether are invalid per the CORS spec. When sending cookies, you must specify exact origins. - Blueprint-level CORS misconfigured —
CORS(app)doesn’t always cover blueprints added after initialization without explicit configuration. - Route returns an error (4xx/5xx) — if the route handler throws before returning, Flask might return a response without CORS headers, so the browser sees both the status error and the CORS error.
Version History That Changes the Failure Mode
Flask and flask-cors have moved enough across the last few releases that copy-pasting an old snippet from Stack Overflow into a new project is a common cause of “it should work but doesn’t.”
Flask 1.0 (April 2018). Established the app.route(..., methods=['GET','POST']) signature, modern blueprints, and the application factory pattern. flask-cors 3.x worked unchanged.
Flask 1.1 (July 2019). Added flask routes for inspecting URL rules. Did not change CORS behavior.
Flask 2.0 (May 2021). Added async def view support and method-specific decorators (@app.get, @app.post). Werkzeug 2.0 came along, and flask-cors had to update to keep up with the new request/response objects. If you pin flask-cors < 3.0.10 against Flask 2.x, after-request hooks misbehave on async views — upgrade to flask-cors >= 4.0.0.
Flask 2.2 (August 2022). Deprecated the legacy app.before_first_request hook used by some CORS-related boot scripts. Tightened blueprint nesting rules — child blueprints inherit the parent’s URL prefix, which interacts with CORS(blueprint, ...) in ways that did not exist in 1.x.
Flask 3.0 (September 2023). Built on Werkzeug 3 and Click 8.1+. Removed long-deprecated APIs: flask.json.JSONEncoder, flask.Markup, and flask.escape. flask-cors versions older than 4.0.1 import some of these and crash at startup with ImportError. The fix is pip install --upgrade flask-cors to a version that targets Werkzeug 3.
Flask 3.1 (2025). Continues the Werkzeug 3 line, with stricter type hints. Older flask-cors versions that monkey-patched Flask.handle_user_exception need the latest release to keep working.
flask-cors version history. flask-cors 3.x supported Flask 1.x. flask-cors 4.0 (released alongside Flask 2.3) is the first version officially supporting Flask 3 — it also dropped Python 2 compatibility code. flask-cors 5.0 (2024) added stricter validation of the origins parameter, refusing combinations like origins='*' and supports_credentials=True rather than silently accepting them.
Flask-Talisman as a partial replacement. When you also need a Content Security Policy, flask-talisman is often used alongside flask-cors. Talisman 1.x predates Flask 3 and silently strips some headers; Talisman 2.0+ supports Flask 3 and Werkzeug 3.
Browser cookie SameSite default flip (Chrome 80, Firefox 96). Browsers shifted the default SameSite policy from None to Lax over 2020–2022. Cookies issued by Flask without an explicit SameSite=None; Secure attribute stopped being sent on cross-origin requests, even when CORS is configured correctly. If your CORS preflight passes but the session cookie never arrives, the SameSite default — not Flask — is the cause.
Fix 1: Install and Initialize flask-cors Correctly
pip install flask-corsfrom flask import Flask
from flask_cors import CORS
app = Flask(__name__)
# Initialize CORS before registering routes
CORS(app)
@app.route('/api/data')
def get_data():
return {'data': 'hello'}
if __name__ == '__main__':
app.run(debug=True)CORS(app) with no arguments allows all origins (*) for all routes. Test this first, then restrict to specific origins in production. The wildcard configuration is useful purely as a diagnostic: if CORS(app) makes the browser stop complaining, you have confirmed that the issue is CORS rather than authentication, the wrong endpoint, or a server-side exception. Always replace the wildcard with explicit origins before deploying.
Verify the headers are present:
curl -I -X OPTIONS http://localhost:5000/api/data \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: GET"The response should include Access-Control-Allow-Origin: *.
Fix 2: Restrict to Specific Origins
In production, never use *. Specify the exact allowed origins:
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
# Single origin
CORS(app, origins=['https://app.example.com'])
# Multiple origins
CORS(app, origins=[
'https://app.example.com',
'https://staging.example.com',
'http://localhost:3000', # Dev only
])Or use environment-based configuration:
import os
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
allowed_origins = os.environ.get(
'CORS_ORIGINS',
'http://localhost:3000'
).split(',')
CORS(app, origins=allowed_origins)Set CORS_ORIGINS=https://app.example.com in production and CORS_ORIGINS=http://localhost:3000 in development.
Fix 3: Handle Credentials (Cookies and Authorization Headers)
When the browser sends credentials: 'include' (for cookies) or an Authorization header, you must:
- Set
supports_credentials=True - Specify exact origins (not
*)
CORS(app,
origins=['https://app.example.com', 'http://localhost:3000'],
supports_credentials=True)This sets both Access-Control-Allow-Origin: https://app.example.com (the actual request origin) and Access-Control-Allow-Credentials: true.
Client-side (JavaScript):
// With cookies
fetch('http://localhost:5000/api/profile', {
credentials: 'include',
});
// With Authorization header
fetch('http://localhost:5000/api/profile', {
headers: { Authorization: `Bearer ${token}` },
});Warning: Do not use
origins='*'withsupports_credentials=True— this combination violates the CORS spec and browsers will reject the response. Always specify exact origins when using credentials.
Fix 4: Configure Route-Specific CORS
Apply different CORS settings to different routes:
from flask import Flask
from flask_cors import CORS, cross_origin
app = Flask(__name__)
# Global CORS (no credentials, all origins)
CORS(app)
# Route-level override using decorator
@app.route('/api/public')
@cross_origin() # Allows all origins for this route
def public_endpoint():
return {'data': 'public'}
@app.route('/api/private')
@cross_origin(origins=['https://app.example.com'], supports_credentials=True)
def private_endpoint():
return {'data': 'private'}
# Disable CORS for a specific route (internal use only)
@app.route('/internal/health')
@cross_origin(origins=[])
def health_check():
return {'status': 'ok'}Or configure per-path with resources:
CORS(app, resources={
r'/api/public/*': {'origins': '*'},
r'/api/private/*': {
'origins': ['https://app.example.com'],
'supports_credentials': True,
},
r'/internal/*': {'origins': []},
})Fix 5: Fix Blueprint CORS
When using Flask Blueprints, apply CORS directly to the blueprint or ensure the app-level CORS is initialized before blueprints are registered:
# blueprints/users.py
from flask import Blueprint
from flask_cors import CORS
users_bp = Blueprint('users', __name__)
# Apply CORS to the blueprint directly
CORS(users_bp, origins=['https://app.example.com'])
@users_bp.route('/users')
def get_users():
return {'users': []}# app.py
from flask import Flask
from flask_cors import CORS
from blueprints.users import users_bp
app = Flask(__name__)
CORS(app) # App-level CORS
app.register_blueprint(users_bp, url_prefix='/api')If both app-level and blueprint-level CORS are configured, the blueprint’s config takes precedence for its routes. When using nested blueprints (Flask 2.0+), apply CORS to the leaf blueprint that actually owns the route — the parent blueprint’s CORS config does not automatically propagate to nested children. Verify by running flask routes and confirming the route appears with the expected URL prefix, then send a preflight OPTIONS request with curl to confirm the headers are present at that exact path.
Fix 6: Handle CORS Headers on Error Responses
When your Flask route raises an exception, Flask’s default error handlers return responses without CORS headers — the browser blocks them, hiding the real error from the frontend:
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
# Add CORS headers to error responses
@app.after_request
def after_request(response):
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
return response
@app.errorhandler(404)
def not_found(e):
return jsonify({'error': 'Not found'}), 404
@app.errorhandler(500)
def server_error(e):
return jsonify({'error': 'Internal server error'}), 500Using after_request ensures CORS headers are always added, even to error responses. This lets the browser actually display the error status code rather than showing only “CORS blocked.”
Pro Tip: When debugging CORS issues, check two things in the Network tab: (1) the OPTIONS preflight request — does it return 200 with the right headers? (2) the actual GET/POST — does it include
Access-Control-Allow-Origin? If the preflight passes but the main request is blocked, the issue is with the actual response headers.
Fix 7: Configure Allowed Headers and Methods
If your request includes custom headers (e.g., Authorization, X-API-Key, X-Custom-Header), they must be listed in allow_headers:
CORS(app,
origins=['https://app.example.com'],
allow_headers=['Content-Type', 'Authorization', 'X-API-Key'],
methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
max_age=3600) # Cache preflight for 1 hourFor manual header handling without flask-cors:
from flask import Flask, request, jsonify
app = Flask(__name__)
def add_cors_headers(response):
response.headers['Access-Control-Allow-Origin'] = 'https://app.example.com'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
return response
app.after_request(add_cors_headers)
# Handle preflight manually
@app.before_request
def handle_preflight():
if request.method == 'OPTIONS':
response = app.make_default_options_response()
add_cors_headers(response)
return responseStill Not Working?
Verify flask-cors is actually installed in your virtual environment:
pip show flask-cors
# If not found: pip install flask-corsCheck import — common mistake is importing from the wrong package:
# Wrong
from flask.cors import CORS
# Correct
from flask_cors import CORSTest without the browser to isolate whether it’s a CORS issue or an application error:
# Direct test — no browser CORS check
curl http://localhost:5000/api/data
# Simulated CORS request
curl -H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization" \
-X OPTIONS \
-v http://localhost:5000/api/dataIf curl works but the browser doesn’t, it’s definitely CORS. If curl also fails, it’s an application error unrelated to CORS.
Check for middleware stripping headers — if Flask runs behind nginx or a reverse proxy, the proxy might strip Access-Control-* headers or not forward the Origin header. Check nginx configuration for proxy_pass_header or add_header directives.
Check the SameSite cookie attribute. If your preflight returns 200 with the right Access-Control-Allow-Origin and Access-Control-Allow-Credentials: true, but the actual request still arrives without session cookie, set app.config['SESSION_COOKIE_SAMESITE'] = 'None' and app.config['SESSION_COOKIE_SECURE'] = True. Modern browsers refuse to send cookies cross-origin without these attributes.
Check flask-cors version against Flask version. Run pip show flask flask-cors. If Flask is 3.x and flask-cors is below 4.0.1, you will see import errors at startup. Upgrade with pip install --upgrade flask flask-cors. Conversely, flask-cors 5.x with Flask 1.1 may emit deprecation warnings that mask the real issue.
Check for double Access-Control-Allow-Origin headers. When both flask-cors and a reverse proxy (nginx, CloudFront, Cloudflare) add the header, the browser sees two values separated by a comma and rejects the response. Disable one source — usually drop the add_header Access-Control-Allow-Origin from nginx and let Flask own the headers.
For related Flask issues, see Fix: Flask 404 Not Found, Fix: Express CORS Not Working, Fix: CORS Preflight Request Blocked, and Fix: CORS Credentials Error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Kafka Consumer Not Receiving Messages, Connection Refused, and Rebalancing Errors
How to fix Apache Kafka issues — consumer not receiving messages, auto.offset.reset, Docker advertised.listeners, max.poll.interval.ms rebalancing, MessageSizeTooLargeException, and KafkaJS errors.
Fix: Flask Route Returns 404 Not Found
How to fix Flask routes returning 404 — trailing slash redirect, Blueprint prefix issues, route not registered, debug mode, and common URL rule mistakes.
Fix: Marshmallow Not Working — Schema Errors, Load vs Dump, and Field Validation
How to fix Marshmallow errors — Schema not validated on dump, ValidationError messages format, unknown field handling, missing vs default, post_load object construction, and Marshmallow 3 to 4 migration.
Fix: Gunicorn Not Working — Worker Timeout, Boot Errors, and Signal Handling
How to fix Gunicorn errors — WORKER TIMEOUT killed, ImportError cannot import app, worker class not found, connection refused 502 behind nginx, graceful reload not working, and sync vs async worker selection.