Django CORS error on localhost (tested)
Django never sends Access-Control-Allow-Origin on its own, so a React/Vue dev server on localhost:3000 gets a CORS error. Install django-cors-headers, put CorsMiddleware at the top, and list the exact origin.
Setup
A single-file Django app with one JSON view at /api/; show() sends a request with an Origin header (as a page on localhost:3000 would) and prints the CORS headers of the response:
import sys
import django
from django.conf import settings
def setup(**extra):
settings.configure(ROOT_URLCONF=__name__, SECRET_KEY='dev', ALLOWED_HOSTS=['*'], **extra)
django.setup()
from django.urls import path
from django.http import JsonResponse
def api(request): return JsonResponse({'ok': True})
urlpatterns = [path('api/', api)]
def show(c, method, origin, **hdr):
r = getattr(c, method)('/api/', headers={'Origin': origin, **hdr})
print(f'{method.upper():7} Origin {origin:24} -> {r.status_code}',
{k: v for k, v in r.headers.items() if k.lower().startswith('access-control') or k == 'Vary'})
1. Reproduce it: no CORS package
from common import *
setup(MIDDLEWARE=[]) # no CORS package
from django.test import Client
c = Client()
show(c, 'get', 'http://localhost:3000')
show(c, 'options', 'http://localhost:3000', **{'Access-Control-Request-Method': 'POST'})
Output:
GET Origin http://localhost:3000 -> 200 {}
OPTIONS Origin http://localhost:3000 -> 200 {}
Django itself never adds Access-Control-Allow-Origin. The server answers 200 (a plain function view even accepts OPTIONS), but without that header the browser blocks your frontend from reading the response: the request "works" in the Network tab and fails in JavaScript.
2. Fix: django-cors-headers
pip install django-cors-headers, then:
from common import *
setup(INSTALLED_APPS=['corsheaders'],
MIDDLEWARE=['corsheaders.middleware.CorsMiddleware', # as high as possible
'django.middleware.common.CommonMiddleware'],
CORS_ALLOWED_ORIGINS=['http://localhost:3000'])
from django.test import Client
c = Client()
show(c, 'get', 'http://localhost:3000')
show(c, 'get', 'http://127.0.0.1:3000') # different origin than localhost
show(c, 'options', 'http://localhost:3000', **{'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'content-type'})
Output:
GET Origin http://localhost:3000 -> 200 {'Vary': 'origin', 'access-control-allow-origin': 'http://localhost:3000'}
GET Origin http://127.0.0.1:3000 -> 200 {'Vary': 'origin'}
OPTIONS Origin http://localhost:3000 -> 200 {'Vary': 'origin', 'access-control-allow-origin': 'http://localhost:3000', 'access-control-allow-headers': 'accept, authorization, content-type, user-agent, x-csrftoken, x-requested-with', 'access-control-allow-methods': 'DELETE, GET, OPTIONS, PATCH, POST, PUT', 'access-control-max-age': '86400'}
- Add
'corsheaders'toINSTALLED_APPSand putCorsMiddlewareas high as possible inMIDDLEWARE, aboveCommonMiddleware. http://localhost:3000andhttp://127.0.0.1:3000are different origins. The second gets onlyVary: originand no allow header, so the browser still reports a CORS error. List each origin you really open the frontend on.- The preflight (
OPTIONSwithAccess-Control-Request-Method) is answered by the middleware with the default allowed methods and headers, which already includecontent-type,authorizationandx-csrftoken.
3. A trailing slash in CORS_ALLOWED_ORIGINS
from common import *
from django.core.checks import run_checks
setup(INSTALLED_APPS=['corsheaders'], MIDDLEWARE=['corsheaders.middleware.CorsMiddleware'],
CORS_ALLOWED_ORIGINS=['http://localhost:3000/']) # trailing slash in the setting
for m in run_checks():
print(m)
Output:
?: (corsheaders.E014) Origin 'http://localhost:3000/' in CORS_ALLOWED_ORIGINS should not have path
Origins have no path, so 'http://localhost:3000/' is invalid. django-cors-headers reports it as system check corsheaders.E014, which runserver and manage.py check show at startup: scroll up in the server output.
If the frontend sends cookies (session auth), you also need CORS_ALLOW_CREDENTIALS = True plus credentials: 'include' in fetch, and for POSTs the CSRF setup in Django CSRF verification failed.
Related: FastAPI CORS error on localhost, Flask CORS error on localhost, CORS error messages explained.
Frequently asked questions
Why do I get a CORS error from Django on localhost?
Django adds no Access-Control-Allow-Origin header. Install django-cors-headers and add the frontend origin, e.g. http://localhost:3000, to CORS_ALLOWED_ORIGINS.
Is localhost the same origin as 127.0.0.1 for CORS?
No. In the test, http://127.0.0.1:3000 got no allow header when only http://localhost:3000 was listed.
What is corsheaders.E014?
A system check error: an entry in CORS_ALLOWED_ORIGINS has a path, often just a trailing slash. Remove it.
Written with AI assistance. Measured on 2026-10-23 with Django 6.1.2, django-cors-headers 4.9.0 and Python 3.14.7 (python:3-alpine in Docker, Django test client). Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; Python, Django and django-cors-headers are free.