Skip to content

Fix: Flask CORS Not Working

FixDevs · (Updated: )

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-cors not installed or not initializedCORS(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 credentialsorigins='*' and supports_credentials=True together are invalid per the CORS spec. When sending cookies, you must specify exact origins.
  • Blueprint-level CORS misconfiguredCORS(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-cors
from 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:

  1. Set supports_credentials=True
  2. 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='*' with supports_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'}), 500

Using 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 hour

For 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 response

Still Not Working?

Verify flask-cors is actually installed in your virtual environment:

pip show flask-cors
# If not found: pip install flask-cors

Check import — common mistake is importing from the wrong package:

# Wrong
from flask.cors import CORS

# Correct
from flask_cors import CORS

Test 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/data

If 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles