python -m http.server with CORS (tested)
python -m http.server sends no CORS headers, so a page on another port can't fetch() from it. The common fix -- override end_headers() to add Access-Control-Allow-Origin: * -- works for a plain GET. We measured in Chromium 153 that it still fails for any request with an Authorization header or a JSON Content-Type, because those trigger an OPTIONS preflight that SimpleHTTPRequestHandler answers with 501. Below is a drop-in script that handles both.
1. python -m http.server has no CORS option
python3 -m http.server --help in Python 3.13.5 lists no CORS flag, and SimpleHTTPRequestHandler sends no Access-Control-Allow-Origin header. A page on another origin -- and a different port counts, so localhost:3000 fetching from localhost:8000 is cross-origin -- gets this in Chromium 153's console:
Access to fetch at 'http://127.0.0.1:8964/data.json' from origin 'http://127.0.0.1:8966' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
2. A drop-in replacement with CORS (and preflight)
Save as cors_server.py and run python3 cors_server.py preflight 8000 from the directory you want to serve:
import sys
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
class CORSRequestHandler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Access-Control-Allow-Origin", "*")
super().end_headers()
class CORSPreflightHandler(CORSRequestHandler):
def do_OPTIONS(self):
self.send_response(204)
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
self.send_header("Access-Control-Max-Age", "600")
self.end_headers()
handler = {"plain": SimpleHTTPRequestHandler, "cors": CORSRequestHandler,
"preflight": CORSPreflightHandler}[sys.argv[1]]
ThreadingHTTPServer(("127.0.0.1", int(sys.argv[2])), handler).serve_forever()
Overriding end_headers() adds the header to every response the handler sends, including 404s and directory listings. do_OPTIONS answers the browser's preflight.
3. What the browser allowed, per handler
We served the same data.json with each handler on its own port and called fetch() from a page on http://127.0.0.1:8966 in headless Chromium 153:
plain GET BLOCKED: Failed to fetch
plain GET + Authorization header BLOCKED: Failed to fetch
plain POST application/json BLOCKED: Failed to fetch
cors GET 200 {"ok": true}
cors GET + Authorization header BLOCKED: Failed to fetch
cors POST application/json BLOCKED: Failed to fetch
preflight GET 200 {"ok": true}
preflight GET + Authorization header 200 {"ok": true}
preflight POST application/json 501 <!DOCTYPE HTML>...
Measured:
- Header only (
cors) fixes a plainGETbut nothing that needs a preflight. Adding anAuthorizationheader, a custom header, orContent-Type: application/jsonmakes the browser sendOPTIONSfirst, andSimpleHTTPRequestHandlerhas nodo_OPTIONS, so it answers501 Unsupported method ('OPTIONS'). Chromium reports:Response to preflight request doesn't pass access control check: It does not have HTTP ok status.Most copy-paste snippets stop at the header and hit this. - With
do_OPTIONS(preflight) the preflight passes. The POST then reaches the server and gets a readable501, becauseSimpleHTTPRequestHandleris a static file server with nodo_POST. That's no longer a CORS problem: add your owndo_POSTif you need one.
4. Check it with curl
$ curl -s -i -X OPTIONS http://127.0.0.1:8965/data.json # header-only handler
HTTP/1.0 501 Unsupported method ('OPTIONS')
$ curl -s -i -X OPTIONS http://127.0.0.1:8967/data.json -H 'Origin: http://127.0.0.1:8966'
HTTP/1.0 204 No Content
Server: SimpleHTTP/0.6 Python/3.13.5
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
Access-Control-Allow-Origin: *
(Date header omitted.) Access-Control-Allow-Origin: * is fine for local development of public, non-credentialed files. It does not work with fetch(..., {credentials: 'include'}) -- that needs the exact origin echoed back plus Access-Control-Allow-Credentials: true -- and don't use it on a server that serves anything private: http.server is not meant for production.
Related: CORS preflight (OPTIONS) explained and CORS error messages explained.
Frequently asked questions
Is there a --cors flag for python -m http.server?
Not in Python 3.13.5: python3 -m http.server --help lists no CORS option. Use a small subclass like the script above.
Why does my fetch still fail after adding Access-Control-Allow-Origin?
If the request has an Authorization header, a custom header, or Content-Type: application/json, the browser sends an OPTIONS preflight first. SimpleHTTPRequestHandler has no do_OPTIONS and replies 501 Unsupported method ('OPTIONS'), which Chromium reports as 'It does not have HTTP ok status'. Add do_OPTIONS as shown.
Does a different port count as a different origin?
Yes. Origin is scheme + host + port, so http://localhost:3000 and http://localhost:8000 are different origins. We measured the block between two 127.0.0.1 ports.
Can I use * with cookies or credentials?
No. Browsers reject Access-Control-Allow-Origin: * for credentialed requests; you must echo the specific Origin and send Access-Control-Allow-Credentials: true.
Why do I get 501 on POST after fixing CORS?
SimpleHTTPRequestHandler only implements GET and HEAD (plus the do_OPTIONS you added). A 501 on POST is the server saying it has no POST handler; write a do_POST method in your subclass.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's http.server (SimpleHTTPRequestHandler, ThreadingHTTPServer), headless Chromium 153.0.8010.12 via Playwright, and curl, serving one JSON file on separate localhost ports to a page on another port. PlainHeaders is our extension; Python, Chromium and curl are free.