FastAPI Essentials

Course Content

FastAPI Essentials

1 sections · 32 lessons

Can you describe how you would handle CORS (Cross-Origin Resource Sharing) in FastAPI?


Two CORS settings, one request from evil.exampleExplicit origin list• Preflight from evil.example: 400• No allow-origin header sent• Browser hides the response• Server still ran the requestWildcard plus credentials• Preflight from evil.example: 200• Origin echoed back, credentials true• Any site reads cookie-backed data• Looks like it just works
Starlette answers a wildcard with credentials by echoing whatever origin asked, which quietly trusts every website.

What you need to know

By default a browser lets a page send a request to another origin but does not let the page's JavaScript read the response. The server opts in with Access-Control-Allow-* headers. For requests with JSON bodies, custom headers or methods other than GET/POST, the browser first sends a preflight OPTIONS request asking "may I?".

Python
from fastapi import FastAPIfrom fastapi.middleware.cors import CORSMiddlewareapp = FastAPI()app.add_middleware(    CORSMiddleware,    allow_origins=["https://app.example.com"],      # from config, per environment    allow_credentials=True,                         # only if you use cookies    allow_methods=["GET", "POST"],    allow_headers=["authorization", "content-type"],    expose_headers=["x-request-id"],                # let browser JS read this header    max_age=600,                                    # cache the preflight for 10 minutes)

Real results with this configuration:

Text
preflight from https://app.example.com  -> 200, Access-Control-Allow-Origin: https://app.example.compreflight from https://evil.example     -> 400, no allow-origin headerPOST from https://evil.example          -> 200 {'answer': 'hi'}, no allow-origin header

The last line is the key lesson. The server still ran the request; only the browser refused to show the response to the evil page's JavaScript. CORS protects users' browsers from reading data, not your server from being called.

The wildcard trap

The CORS standard forbids Access-Control-Allow-Origin: * together with credentials. Starlette handles allow_origins=["*"] plus allow_credentials=True by echoing back whatever origin asked:

Text
allow_origins=["*"], allow_credentials=True, request from https://evil.example-> Access-Control-Allow-Origin: https://evil.example   Access-Control-Allow-Credentials: true

So the request does not fail — it succeeds for every website, with the user's cookies. That is effectively no CORS protection for cookie-authenticated APIs. Always list origins explicitly when credentials are on.

Other gotchas

  • Custom response headers such as x-request-id are invisible to browser JavaScript unless listed in expose_headers.
  • A failed preflight appears in the browser console as a vague network error. Check the OPTIONS request in the network tab.
  • Streaming from the browser (fetch with SSE) follows the same CORS rules.

A real-life example

A startup builds a chat widget that customers embed on their own websites; it calls the startup's FastAPI backend. During a demo, the widget failed on a customer's site with a CORS error. A developer "fixed" it with allow_origins=["*"], allow_credentials=True. It worked everywhere — including, as a later security audit showed, on any site that wanted to call the dashboard API with a logged-in admin's session cookie.

The final design split the two: the public widget API uses per-customer API keys in a header, no cookies, and allows the origins each customer registered (stored in the database and checked by a custom middleware). The admin dashboard API allows exactly one origin, https://dashboard.example.com, with credentials.

Follow-up questions to expect

  • "Does CORS stop curl or Postman?" — No. Only browsers enforce it. Non-browser clients ignore CORS entirely.
  • "How do you allow many subdomains?" — allow_origin_regex=r"https://.*\.example\.com", written carefully so it cannot match example.com.evil.net.
  • "Why does my preflight fail when the GET works?" — The actual request has a header or method not in allow_headers or allow_methods, such as authorization.