CORS & Cross-Origin Requests

Reviewed & published by Brayan K

CORS is a browser security mechanism that decides whether JavaScript on one website is allowed to read a response from an API hosted on a different origin.

Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll learn what the Access-Control-Allow-* headers do, how preflight OPTIONS requests work, and how to enable CORS in Flask — both by hand and with the Flask-CORS extension.

What You'll Learn in This Lesson

1 The Allow-Origin Header

An origin is the combination of scheme, host, and port — https://app.example.com. By default, the browser's same-origin policy stops JavaScript loaded from one origin from reading a response served by another. To permit it, the server adds an Access-Control-Allow-Origin header naming the allowed origin (or * for any).

You can add that header yourself with an after_request hook that runs after every view. The runnable example sets the header and inspects it with the test client.

# app.py — add the CORS header by hand (runnable via test_client)
from flask import Flask, jsonify

app = Flask(__name__)

@app.after_request
def add_cors_headers(response):
    # this is exactly what Flask-CORS does for you
    response.headers["Access-Control-Allow-Origin"] = "*"
    return response

@app.route("/api/data")
def data():
    return jsonify(message="hello from the API")

with app.test_client() as client:
    r = client.get("/api/data")
    print("Allow-Origin:", r.headers.get("Access-Control-Allow-Origin"))   # *
    print("body:", r.data.decode().strip())

# ✅ Expected output:
# Allow-Origin: *
# body: {"message":"hello from the API"}

Output: Allow-Origin: * and the JSON body. With that header present, a browser will let cross-origin JavaScript read the response.

2 Methods, Headers & the Preflight Request

A simple GET only needs Allow-Origin. But for requests with custom headers (like Authorization) or methods like PUT and DELETE, the browser first sends a preflight OPTIONS request asking permission. The server must answer with Access-Control-Allow-Methods and Access-Control-Allow-Headers.

The example adds all three headers and shows a preflight OPTIONS call returning them, so the browser would then allow the real request.

# app.py — answer a preflight OPTIONS request (runnable)
from flask import Flask, jsonify

app = Flask(__name__)

@app.after_request
def add_cors_headers(response):
    response.headers["Access-Control-Allow-Origin"] = "https://app.example.com"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
    return response

# allow OPTIONS so the preflight reaches a view
@app.route("/api/items", methods=["GET", "POST", "OPTIONS"])
def items():
    return jsonify(ok=True)

with app.test_client() as client:
    pre = client.options("/api/items")     # the browser's preflight
    print("preflight Allow-Methods:", pre.headers.get("Access-Control-Allow-Methods"))
    print("preflight Allow-Headers:", pre.headers.get("Access-Control-Allow-Headers"))
    print("Allow-Origin:", pre.headers.get("Access-Control-Allow-Origin"))

# ✅ Expected output:
# preflight Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
# preflight Allow-Headers: Content-Type, Authorization
# Allow-Origin: https://app.example.com

Output: the allowed methods, the allowed headers, and the specific origin — the trio the browser checks before sending the real PUT, POST, or DELETE.

3 The Flask-CORS One-Liner

Writing the after_request hook by hand teaches you what is happening, but in production you reach for the Flask-CORS extension. It handles all the headers, the preflight, per-route configuration, and credentials with a single line. Here is the production setup, shown read-only because it relies on the flask_cors package.

🎯 Your Turn: Fill in the Blanks

Complete the CORS hook. Replace each ___ to allow a specific origin and the POST method.

# 🎯 YOUR TURN: replace each ___ to enable CORS
from flask import Flask, jsonify

app = Flask(__name__)

@app.___request                                   # which hook runs after every view?
def add_cors(response):
    response.headers["Access-Control-Allow-___"] = "https://my-frontend.com"   # which origin?
    response.headers["Access-Control-Allow-Methods"] = "GET, ___"              # also allow POST
    return response

@app.route("/api/ping")
def ping():
    return jsonify(pong=True)

with app.test_client() as c:
    r = c.get("/api/ping")
    print("origin:", r.headers.get("Access-Control-Allow-Origin"))   # https://my-frontend.com
    print("methods:", r.headers.get("Access-Control-Allow-Methods")) # GET, POST

Common Errors & Quick Reference

❌ "No 'Access-Control-Allow-Origin' header is present"

The browser blocked the read because the server sent no Allow-Origin header. Fix it on the server — add the header or enable Flask-CORS — not in the front-end.

❌ Preflight fails on a PUT or DELETE

Your server did not answer the OPTIONS preflight with Allow-Methods/Allow-Headers. Allow OPTIONS and return those headers, or let Flask-CORS do it.

❌ Credentials with a wildcard origin

You cannot use Allow-Origin: * together with credentials. Name a specific origin and add Allow-Credentials: true.

TaskHeader
Allow an originAccess-Control-Allow-Origin
Allow methodsAccess-Control-Allow-Methods
Allow headersAccess-Control-Allow-Headers
Allow cookiesAccess-Control-Allow-Credentials

🏆 Mini Challenge: Origin Allowlist

Echo back the request's Origin only if it is on an allowlist.

# 🏆 Mini Challenge: reflect only trusted origins
from flask import Flask, request, jsonify

app = Flask(__name__)
ALLOWED = {"https://app.example.com", "https://admin.example.com"}

@app.after_request
def cors(response):
    origin = request.headers.get("Origin")
    if origin in ALLOWED:
        response.headers["Access-Control-Allow-Origin"] = origin   # reflect trusted origin
    return response

@app.route("/api/data")
def data():
    return jsonify(ok=True)

with app.test_client() as c:
    good = c.get("/api/data", headers={"Origin": "https://app.example.com"})
    print("trusted ->", good.headers.get("Access-Control-Allow-Origin"))
    bad = c.get("/api/data", headers={"Origin": "https://evil.com"})
    print("untrusted ->", bad.headers.get("Access-Control-Allow-Origin"))

❓ Frequently Asked Questions

Lesson complete — your API plays nicely with front-ends!

You understand origins and the same-origin policy, the Access-Control-Allow-* headers, preflight OPTIONS requests, credentials, and the Flask-CORS one-liner.

Practice quiz

What does CORS stand for?

  • Cross-Origin Resource Sharing
  • Central Origin Routing System
  • Cookie Origin Request Security
  • Cross-Origin Response Standard

Answer: Cross-Origin Resource Sharing. CORS is Cross-Origin Resource Sharing, a browser mechanism for cross-origin requests.

What three parts make up an origin?

  • host, path, query
  • scheme, host, and port
  • domain and cookie only
  • method and URL

Answer: scheme, host, and port. An origin is scheme + host + port, e.g. https://app.example.com.

Which header tells the browser a cross-origin read is allowed?

  • Allow-Cross-Origin
  • X-CORS-Allow
  • Access-Control-Allow-Origin
  • Origin-Permitted

Answer: Access-Control-Allow-Origin. Access-Control-Allow-Origin names the origin (or *) that may read the response.

Where do you fix a CORS error?

  • in the browser settings
  • in the front-end fetch call
  • by disabling JavaScript
  • on the server

Answer: on the server. The browser enforces CORS, but the server grants permission via the Allow headers.

What HTTP method does a preflight request use?

  • OPTIONS
  • GET
  • HEAD
  • TRACE

Answer: OPTIONS. The browser sends an OPTIONS preflight before non-simple requests.

When does the browser send a preflight request?

  • on every GET
  • for custom headers or methods like PUT/DELETE
  • only over HTTP
  • never for JSON

Answer: for custom headers or methods like PUT/DELETE. Non-simple requests (custom headers, PUT/DELETE) trigger a preflight.

Which headers must a preflight response include for a PUT?

  • only Allow-Origin
  • Content-Type only
  • Allow-Methods and Allow-Headers
  • Set-Cookie

Answer: Allow-Methods and Allow-Headers. The preflight answer needs Access-Control-Allow-Methods and Allow-Headers.

Why can't you use Allow-Origin: * with credentials?

  • The wildcard is rejected with credentials
  • It is too slow
  • Credentials require GET
  • The port must match

Answer: The wildcard is rejected with credentials. With credentials you must name a specific origin; * is rejected by the browser.

Which header must be true to allow cookies cross-origin?

  • Access-Control-Allow-Cookies
  • Allow-Origin-Credentials
  • Access-Control-Allow-Credentials
  • Set-Credentials

Answer: Access-Control-Allow-Credentials. Access-Control-Allow-Credentials: true permits credentialed requests.

How do you enable CORS for a whole app with Flask-CORS?

  • app.cors()
  • CORS(app)
  • enable_cors(app)
  • app.config['CORS'] = True

Answer: CORS(app). Importing CORS from flask_cors and calling CORS(app) enables it everywhere.

Continue this course

Frequently asked questions

What is CORS and why does it exist?

CORS (Cross-Origin Resource Sharing) is a browser security mechanism. By default the same-origin policy blocks JavaScript on one origin (scheme + host + port) from reading responses from a different origin. CORS lets a server opt in by sending Access-Control-Allow-* headers that tell the browser the cross-origin request is permitted.

Why is CORS a server setting if the browser enforces it?

The browser is the enforcer, but the server is the one that grants permission. The browser will only expose a cross-origin response to JavaScript if the server's response includes the right Access-Control-Allow-Origin header. So you fix CORS errors on the server, not in the front-end.

What is a preflight request?

For non-simple requests (custom headers, methods like PUT or DELETE, or JSON bodies), the browser first sends an OPTIONS request — the preflight — asking whether the real request is allowed. The server answers with Access-Control-Allow-Methods and Access-Control-Allow-Headers. Only if that passes does the browser send the actual request.

How do credentials work with CORS?

If the front-end sends cookies or auth headers with credentials: 'include', the server must respond with Access-Control-Allow-Credentials: true AND a specific origin (not the * wildcard). Using * together with credentials is rejected by the browser for security.

Related lessons