Last modified: Oct 14, 2026

Configure CORS in Django with django-cors-headers

Installing django-cors-headers is the easy part. The configuration is where most projects get stuck.

A wrong origin string blocks your frontend. A missing header breaks uploads. Wildcard origins with credentials get rejected by the browser outright.

This guide focuses on configuration. You will learn every setting that matters and the exact values that work in development and production.

What django-cors-headers Actually Does

The package sits in your middleware stack. It inspects each incoming request and decides whether the browser should be allowed to read the response.

If the origin is permitted, it adds response headers. If not, the browser blocks the response on its own.

The server still processes the request either way. CORS is enforced by the browser, not by Django. That detail matters when you debug.

A request can return HTTP 200 and still fail in the browser. The failure happens after the response arrives.

The Two Configuration Layers

Configuration happens in two places. First, the middleware entry controls when the headers are added. Second, the settings control which origins and headers are allowed.

The middleware is registered once in your settings file.

 # settings.py MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", ] 

Placement is critical. The middleware must run before anything that can generate a response.

If it sits below the common middleware, some responses skip the CORS headers entirely.

If you have not added the package yet, follow this guide on how to install django-cors-headers first. It covers the pip command and the app registration.

Setting Allowed Origins

This is the setting you will adjust most often. It lists the exact origins permitted to call your API.

 # settings.py CORS_ALLOWED_ORIGINS = [ "http://localhost:3000", "http://127.0.0.1:3000", "https://app.example.com", ] 

Each entry must include the scheme, host, and port. There is no trailing slash.

Browsers treat localhost and 127.0.0.1 as separate origins. During development, list both.

A typo in this list is the single most common cause of CORS failures. Compare the string character by character against the browser error message.

Development Versus Production

You do not want the same origin list in every environment. Development needs flexibility. Production needs strict rules.

Use an environment variable to switch behavior cleanly.

 # settings.py import os DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True" if DEBUG: CORS_ALLOW_ALL_ORIGINS = True else: CORS_ALLOWED_ORIGINS = [ "https://app.example.com", ] 

The allow-all flag disables origin checking entirely. It is convenient on a laptop and dangerous on a server.

Never ship this flag enabled. It lets any website on the internet call your API with the user's cookies attached.

Allowing Credentials

Cookies and HTTP authentication require an extra opt-in. Without it, the browser strips credentials from the request.

 # settings.py CORS_ALLOW_CREDENTIALS = True 

With credentials enabled, two rules apply. The origin must be listed explicitly. Wildcards are rejected by the browser.

You also cannot combine credentials with the allow-all flag. Django will not raise an error, but the browser will refuse the response.

Session-based authentication and social login flows often need this. If your frontend authenticates users through a separate domain, review how to set up social login with django-allauth so the cookie and CORS rules line up.

Limiting CORS to Specific Paths

Not every URL needs CORS. Your admin panel and internal endpoints can stay closed.

Use a URL pattern to scope the middleware to API routes only.

 # settings.py CORS_URLS_REGEX = r"^/api/.*$" 

Only requests matching that pattern receive CORS headers. Everything else behaves as if the middleware were absent.

This reduces your exposed surface area. It also keeps the admin interface invisible to cross-origin scripts.

Adding Custom Request Headers

Frontends often send headers that are not in the default list. A tracking ID or a custom auth scheme will trigger a preflight failure.

 # settings.py from corsheaders.defaults import default_headers CORS_ALLOW_HEADERS = list(default_headers) + [ "x-request-id", "x-client-version", ] 

Start from the defaults and extend the list. Replacing it entirely is a common mistake that removes authorization and content-type.

Exposing Response Headers

By default, the browser only exposes a small set of response headers to JavaScript. Custom headers stay hidden.

 # settings.py CORS_EXPOSE_HEADERS = [ "x-total-count", "x-page-size", ] 

Add any header your frontend needs to read. Pagination metadata is the classic example.

Without this setting, the header exists on the response but returns as null in JavaScript.

Caching Preflight Requests

Browsers send an OPTIONS request before certain cross-origin calls. Each one costs a round trip.

You can cache the answer for a period of time to cut that overhead.

 # settings.py CORS_PREFLIGHT_MAX_AGE = 86400 

A value of 86400 seconds means the browser remembers the preflight result for a full day.

Some browsers cap this value on their own. The default in django-cors-headers is already 86400, so only change it if you have a reason.

Using Regex for Dynamic Subdomains

Applications that give each customer a subdomain cannot list every origin. A regex solves that.

 # settings.py CORS_ALLOWED_ORIGIN_REGEXES = [ r"^https://\w+\.example\.com$", ] 

This pattern matches any single-label subdomain over https. It covers staging, demo, and tenant domains in one line.

Write the pattern tightly. A loose regex like .*example\.com.* matches far more than intended.

Verifying the Configuration

Test the setup before you touch the frontend. A simple curl request shows whether the headers are present.

 curl -I http://localhost:8000/api/items/ \ -H "Origin: http://localhost:3000" 

A working configuration returns the matching header.

 HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:3000 Vary: Origin Content-Type: application/json 

The Vary header tells caches that the response depends on the origin. Django adds it for you.

Now test the preflight path, which is the one that usually fails.

 curl -X OPTIONS http://localhost:8000/api/items/ \ -H "Origin: http://localhost:3000" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: content-type" -i 

The response should list the permitted methods and headers.

 HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:3000 Access-Control-Allow-Methods: DELETE, GET, OPTIONS, PATCH, POST, PUT Access-Control-Allow-Headers: content-type, authorization Access-Control-Max-Age: 86400 

If a header you need is missing, add it to the allowed list. If the origin is missing, check the spelling in your settings.

Common Configuration Mistakes

Wildcard origins with credentials. The browser rejects this combination. List origins explicitly instead.

Trailing slashes in origin strings. An origin is a scheme, host, and port. A path or slash makes it invalid.

Replacing the default header list. Overwriting the list drops essential headers. Extend it instead.

Middleware in the wrong position. A low position lets some responses bypass the headers.

Forgetting the port. Port 3000 and port 3001 are different origins. Both need entries if both are used.

Testing in the wrong tool. curl ignores CORS entirely. Only browsers enforce it, so verify in the browser network tab.

Allowing everything in production. A copy-pasted development setting is a real security hole.

Best Practices

Keep the origin list minimal. Every entry is a trusted party. Add only the domains that need access.

Drive settings from environment variables. One codebase, different configuration per environment.

Scope CORS to API paths. There is no reason for your admin panel to accept cross-origin reads.

Log blocked origins during rollout. A short trial period shows which legitimate origins you forgot to list.

Retest after every deploy. Domains change, proxies get added, and settings drift. One curl command catches it.

Pin the package version. CORS behavior is security-sensitive. A surprise upgrade is not welcome.

If your project also serves static assets, keep the two middleware layers in mind. The installation of whitenoise in Django places its middleware near the top of the stack, and CORS must still come first.

Conclusion

Configuring CORS in Django comes down to a handful of settings. List your origins. Decide whether credentials are needed. Scope the rules to your API paths.

Test with curl before you test in the browser. The headers tell you everything you need to know.

Keep development flexible and production strict. A single environment variable switch makes that safe.

Do this once, and cross-origin requests stop being a mystery. Your frontend talks to your backend, and your API stays protected from origins you never intended to trust. When the configuration is solid, you can move on to deploy a Django app with Fabric knowing the request layer is already correct.