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:

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.