mrkeyoor.com_
Thu 06 Aug 10:57 UTC
PyPIWeb Backendupdated 06 Aug 2026

flask-cors

flask-cors adds the response headers a browser needs before it will let JavaScript on one origin read a response from your Flask app on another. Import CORS, call CORS(app), and the extension attaches an after_request hook that stamps Access-Control-Allow-Origin and friends onto every response, plus a handler that answers the OPTIONS preflight request browsers send before non-simple calls. You can scope it: a resources dictionary maps URL patterns to their own settings, and a @cross_origin() decorator configures a single view. The library's stated philosophy is that when you want CORS you usually want it turned on for a whole domain, so the defaults are wide open and narrowing them is your job.

Verdict

For a Flask API that a browser calls from another origin, this is the standard answer and it works with one line. Treat that one line as a starting point, not a finish line: set explicit origins, never pair a wildcard with credentials, and make sure nothing in front of Flask is setting the same headers.

API stability4/5CORS(app), the resources dictionary, and @cross_origin have kept the same shape for years. The deductions are behavioral rather than syntactic: 5.0.0 flipped the private-network default and 6.0.0 changed which overlapping resource pattern takes precedence, so upgrades can change behavior without changing your code.
Docs4/5The docs site has separate pages for the extension and the decorator with a full option table, and the README documents the debug logger, which is the fastest route to an answer when preflight fails. It is thinner on the security reasoning behind the defaults than a CORS library should be.
Maintenance4/56.0.5 shipped in June 2026 with the repository pushed the next day, and only 20 issues are open (39 counting PRs), so the backlog is genuinely small. It runs on essentially one maintainer plus drive-by contributors, and the 2024 security reports needed two breaking releases to resolve.
Ecosystem5/5Around 14.1 million downloads a week and the default recommendation in nearly every Flask tutorial and Stack Overflow answer about cross-origin errors, so any Flask developer you hire will recognize it immediately.

Use it if

  • You have a Flask API and a separate frontend origin (a Vite dev server on port 5173, a React app on a different domain, a mobile web build) and the browser console is showing blocked-by-CORS errors
  • You need different rules per path: public read endpoints open to any origin, an /admin/* prefix limited to your own domain, each with its own allowed methods and headers
  • Your API sends cookies or Authorization headers cross-origin and you need supports_credentials plus a correctly echoed single origin, which is fiddly enough by hand that a library is worth it
  • You want preflight handling done for you, including caching preflights with max_age so browsers stop sending an OPTIONS request before every PUT
Skip it if

Setup reality

pip install flask-cors is small: it needs flask>=0.9, Werkzeug>=0.7, and typing_extensions on Python older than 3.11, and supports Python 3.9 through 3.13. CORS(app) genuinely works in one line. The friction is everything after that. Ordering matters, because CORS(app) walks the app's registered URL rules, so blueprints registered after that call may not pick up the resources patterns you expected; register blueprints first or apply CORS to the blueprint itself. Preflight interacts with Flask's own automatic OPTIONS handling and with strict_slashes, so a route defined as /api/items can preflight-fail for a request to /api/items/ before your view is ever reached. Errors raised by other extensions can bypass the after_request hook unless intercept_exceptions stays on. And when nothing works, the documented move is to turn on the library's own logger with logging.getLogger('flask_cors').level = logging.DEBUG, which prints exactly which resource pattern matched and which headers it decided to send. Version 6.0.0 changed the order in which overlapping resource patterns win, so upgrading from 5.x can silently change which config applies.

Patterns

Turn CORS on for the whole appenable-for-everything

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

@app.route("/")
def hello():
    return "Hello, cross-origin-world!"

This allows every origin and every method on every route. It is the right way to unblock a local frontend during development and the wrong thing to deploy; the next pattern is what production should look like.

Allow only the origins you ownrestrict-origins

CORS(
    app,
    origins=["https://app.example.com", "http://localhost:5173"],
    methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Content-Type", "Authorization"],
)

Origins must include the scheme and the port when it is not the default; https://app.example.com and http://app.example.com are different origins to a browser. allow_headers has to list every custom header your client sends, or the preflight fails on that header alone.

Different rules for different URL prefixesper-resource-config

CORS(app, resources={
    r"/api/public/*": {"origins": "*"},
    r"/api/private/*": {
        "origins": ["https://app.example.com"],
        "supports_credentials": True,
    },
})

Keys are matched as regular expressions against the request path. When two patterns both match, 6.0.0 changed the tie-break to most-specific-first, so an upgrade from 5.x can change which block applies. Keep the patterns non-overlapping if you can.

Configure a single viewdecorator-per-route

from flask_cors import cross_origin

@app.route("/webhook", methods=["POST", "OPTIONS"])
@cross_origin(origins=["https://partner.example.com"], max_age=600)
def webhook():
    return {"ok": True}

The decorator goes below @app.route, closest to the function. Include OPTIONS in the route's methods list, or Flask may answer the preflight itself before the decorator gets a chance to add headers.

Allow cookies and Authorization across originscredentials-and-cookies

CORS(
    app,
    origins=["https://app.example.com"],
    supports_credentials=True,
)

# and on the browser side:
# fetch(url, { credentials: "include" })

Never pair supports_credentials=True with a wildcard origin. The spec rejects that combination, so flask-cors echoes whatever Origin the caller sent, which effectively allows any site to make authenticated requests with your users' cookies. Also set SameSite=None; Secure on the cookies themselves.

Match a family of subdomainsregex-origins

CORS(app, origins=[r"https://.*\.example\.com$"])

Patterns that look like regular expressions are matched with re.match, which anchors at the start but not the end, so leaving off the trailing $ lets https://app.example.com.attacker.net through. Plain strings without regex characters are compared exactly instead, case-insensitively.

Let the browser read your custom response headersexpose-response-headers

CORS(
    app,
    origins=["https://app.example.com"],
    expose_headers=["X-Total-Count", "X-Request-Id", "Content-Disposition"],
)

By default JavaScript can only read a handful of standard response headers. Pagination counts, request ids, and download filenames are invisible to fetch() until you list them here, which is the cause of the classic "the header is in devtools but response.headers has nothing" report.

Stop the browser preflighting every requestcache-preflight

CORS(
    app,
    origins=["https://app.example.com"],
    methods=["GET", "POST", "PATCH", "DELETE"],
    max_age=86400,
)

max_age sets Access-Control-Max-Age in seconds, so the browser reuses the preflight result instead of sending an OPTIONS before every call. Browsers cap it themselves (Chromium at two hours, Safari lower), so a large value is a request, not a promise.

Apply CORS to one blueprintblueprint-scoped

from flask import Blueprint
from flask_cors import CORS

api = Blueprint("api", __name__, url_prefix="/api")
CORS(api, origins=["https://app.example.com"])

app.register_blueprint(api)

Scoping to the blueprint avoids the ordering problem where CORS(app) runs before later blueprints are registered. Version 6.0.5 specifically restored Blueprint in the type signatures, so mypy accepts this again.

Drive settings from app config or environmentconfigure-via-app-config

app.config["CORS_ORIGINS"] = os.environ["ALLOWED_ORIGINS"].split(",")
app.config["CORS_SUPPORTS_CREDENTIALS"] = True
app.config["CORS_EXPOSE_HEADERS"] = ["X-Total-Count"]

CORS(app)

Every keyword argument has a CORS_-prefixed config equivalent, which keeps environment-specific origin lists out of the code. Arguments passed directly to CORS() win over config values, so do not set the same thing in both places.

See which rule matched and what was sentdebug-preflight-failures

import logging

logging.basicConfig()
logging.getLogger("flask_cors").level = logging.DEBUG

This is the documented troubleshooting step and it is far faster than guessing. The log lines show the matched resource pattern and every header the extension decided to add, which immediately tells you whether the problem is your config or something else rewriting the response.

Check nothing in front of Flask is adding the same headersavoid-duplicate-headers

curl -i -X OPTIONS https://api.example.com/api/items \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST"

# look for more than one of these in the output:
# Access-Control-Allow-Origin: https://app.example.com
# Access-Control-Allow-Origin: *

Two Access-Control-Allow-Origin headers make the browser reject the response even though each one is valid on its own. If your proxy adds them, remove the proxy rules and keep flask-cors, or remove flask-cors and keep the proxy rules, but never both.

Alternatives

PackageRegistryPick it when
starlettePyPIYou are on Starlette or FastAPI, where CORSMiddleware is built in and needs no extra package.
django-cors-headersPyPIThe same job on Django, with settings-based allowlists and regex origin support wired into the middleware stack.
quart-corsPyPIYou moved a Flask app to Quart for async and need the same extension shape against Quart's ASGI request cycle.