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'}

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.