CORS Handling in Python — Core Concepts
The same-origin policy
Browsers enforce a fundamental security rule: JavaScript on one origin can’t freely read responses from a different origin. An origin is the combination of scheme (https), host (api.example.com), and port (443). Change any of those, and it’s a different origin.
This matters because when you’re logged into your bank, any website you visit could silently send requests to your bank’s API — your cookies would be attached automatically. The same-origin policy prevents the malicious site from reading the response.
Where CORS fits in
CORS is the mechanism that relaxes the same-origin policy in controlled ways. It’s a set of HTTP headers that lets a server say “I trust requests from these specific origins.”
When JavaScript on frontend.com makes a fetch request to api.backend.com, the browser sends the request and checks the response headers. If api.backend.com includes Access-Control-Allow-Origin: https://frontend.com, the browser passes the response to JavaScript. Without that header, the browser blocks it.
Simple vs. preflight requests
Not all cross-origin requests are treated equally.
Simple requests use GET, HEAD, or POST with standard content types (form data, plain text). The browser sends them directly and checks CORS headers in the response.
Preflight requests happen when the request uses other methods (PUT, DELETE, PATCH), custom headers, or JSON content type. The browser first sends an OPTIONS request asking “would you accept this?” The server responds with the allowed methods, headers, and origins. Only if the preflight passes does the browser send the actual request.
This distinction exists because simple requests could already be sent by HTML forms before CORS existed. The preflight mechanism protects against new types of requests that JavaScript enabled.
Configuring CORS in Python frameworks
FastAPI uses middleware:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://frontend.com", "https://staging.frontend.com"],
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
allow_credentials=True,
max_age=3600,
)
Flask uses the flask-cors extension. Django uses django-cors-headers. The concepts are identical across frameworks — you specify which origins, methods, and headers are permitted.
Key headers explained
Access-Control-Allow-Origin — which origins can access the resource. Can be a specific origin or * (any origin, but incompatible with credentials).
Access-Control-Allow-Methods — which HTTP methods are permitted for cross-origin requests.
Access-Control-Allow-Headers — which custom headers the client can send.
Access-Control-Allow-Credentials — whether the browser should include cookies and auth headers. When true, Allow-Origin cannot be *; it must be a specific origin.
Access-Control-Max-Age — how long (in seconds) the browser caches preflight results. Setting this to 3600 means the browser won’t send a preflight OPTIONS request for that origin/method combination for an hour.
Common misconception
Developers often think CORS protects the server. It doesn’t. CORS is enforced by the browser. A curl command, a mobile app, or a server-to-server call ignores CORS entirely. CORS protects the user by preventing malicious websites from misusing their browser’s authenticated session. Server-side security (authentication, authorization, rate limiting) is still necessary.
The credentials trap
Setting allow_credentials=True with allow_origins=["*"] won’t work — browsers reject this combination for security. You must list specific origins when credentials are enabled. This catches many developers: their API works in testing (with *), then breaks when they add authentication because they need credentials but forgot to specify exact origins.
The one thing to remember: CORS is a browser-side enforcement of which websites can access your API — configure it with specific origins, not *, especially when authentication is involved.
See Also
- Python Aiohttp Client Understand Aiohttp Client through a practical analogy so your Python decisions become faster and clearer.
- Python Api Client Design Why building your own API client in Python is like creating a TV remote that only has the buttons you actually need.
- Python Api Documentation Swagger Swagger turns your Python API into an interactive playground where anyone can click buttons to try it out — no coding required.
- Python Api Mocking Responses Why testing with fake API responses is like rehearsing a play with stand-ins before the real actors show up.
- Python Api Pagination Clients Why APIs send data in pages, and how Python handles it — like reading a book one chapter at a time instead of swallowing the whole thing.