Skip to content

Fix: Access to fetch has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix 'Access to fetch at ... from origin ... has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource' in JavaScript. Covers Express, Django, Flask, Spring Boot, ASP.NET, nginx, Apache, dev proxies, preflight requests, credentials, and edge cases.

The Error

You make a fetch or XMLHttpRequest from your frontend, and the browser blocks it:

fetch:

Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

XMLHttpRequest:

Access to XMLHttpRequest at 'https://api.example.com/data' from origin
'http://localhost:3000' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

Firefox:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote
resource at https://api.example.com/data. (Reason: CORS header
'Access-Control-Allow-Origin' missing). Status code: 200.

The request actually reaches your server. The browser just refuses to let your JavaScript read the response.

Why This Happens

Browsers enforce the Same-Origin Policy: JavaScript on one origin (http://localhost:3000) cannot read responses from a different origin (https://api.example.com). Two URLs have the same origin only if the protocol, host, and port all match.

These are different origins:

FromToWhy
http://localhost:3000http://localhost:5000Different port
http://example.comhttps://example.comDifferent protocol
https://app.example.comhttps://api.example.comDifferent host

CORS (Cross-Origin Resource Sharing) is the mechanism that relaxes this restriction. The server must include an Access-Control-Allow-Origin header in its response to tell the browser “this origin is allowed to read my response.” If that header is missing, the browser blocks the response.

This is a server-side fix. You cannot bypass CORS from frontend code alone.

Browser History: Why CORS Errors Got Stricter

CORS errors have evolved in ways that catch out engineers returning to a project after a few years. Three browser changes in particular matter.

Chrome’s SameSite default flip (Chrome 80, February 2020). Before Chrome 80, cookies without an explicit SameSite attribute defaulted to SameSite=None, which meant they were sent on cross-site requests. From Chrome 80 onwards the default became SameSite=Lax, so cross-origin XHR/fetch requests stopped sending session cookies unless the server explicitly set SameSite=None; Secure. Code that “worked fine last year” suddenly broke with the same Access-Control-Allow-Origin configuration because the cookies were no longer attached. If you are debugging an authenticated CORS request and your cookies look correct in DevTools but the backend reports no session, this default change is the most likely culprit. See Fix: CORS credentials error for the matching server-side fix.

Safari 14 and ITP (Intelligent Tracking Prevention). Safari started capping the lifetime of Set-Cookie headers set by third-party scripts to 7 days and increasingly treating cross-site cookies as third-party. Safari also tightened its CORS preflight handling around 14.1 so that responses missing the Vary: Origin header would more aggressively cache and produce inconsistent CORS errors across requests. If a fix works in Chrome but fails in Safari, ITP and aggressive caching of preflight responses are almost always the difference.

Fetch spec credentials evolution. Early versions of fetch() defaulted credentials to 'omit', which surprised developers migrating from XHR (which sent cookies on same-origin requests). The default was changed to 'same-origin' (so same-origin requests behave like XHR), but cross-origin requests still require explicit credentials: 'include'. Tutorials written before this change still float around and tell readers to add credentials: 'include' even on same-origin calls, which then unintentionally requires CORS configuration on what should have been a non-CORS request. Match the credentials mode to what the request actually needs.

Fix 1: Express / Node.js (cors Middleware)

Install the cors package:

npm install cors

Add it to your Express app:

const express = require('express');
const cors = require('cors');

const app = express();

// Allow all origins (fine for development, not recommended for production)
app.use(cors());

To allow only specific origins:

app.use(cors({
  origin: 'http://localhost:3000'
}));

To allow multiple specific origins:

app.use(cors({
  origin: ['http://localhost:3000', 'https://myapp.com']
}));

For production, always specify your allowed origins explicitly rather than allowing all.

Without the cors Package

You can set the headers manually:

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  // Handle preflight
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }

  next();
});

Fix 2: Django

Install django-cors-headers:

pip install django-cors-headers

Add it to settings.py:

INSTALLED_APPS = [
    # ...
    'corsheaders',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # Must be before CommonMiddleware
    'django.middleware.common.CommonMiddleware',
    # ...
]

# Allow specific origins
CORS_ALLOWED_ORIGINS = [
    'http://localhost:3000',
    'https://myapp.com',
]

# Or allow all origins (development only)
# CORS_ALLOW_ALL_ORIGINS = True

Important: CorsMiddleware must be placed before CommonMiddleware and any middleware that can generate responses (like CsrfViewMiddleware). If it’s too low in the list, the headers won’t be added.

Fix 3: Flask

Install flask-cors:

pip install flask-cors
from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # Allows all origins

# Or restrict to specific origins
CORS(app, origins=['http://localhost:3000', 'https://myapp.com'])

Fix 4: Spring Boot

Add the @CrossOrigin annotation to a controller:

@CrossOrigin(origins = "http://localhost:3000")
@RestController
public class ApiController {
    @GetMapping("/data")
    public ResponseEntity<String> getData() {
        return ResponseEntity.ok("data");
    }
}

For global configuration, define a WebMvcConfigurer bean:

@Configuration
public class CorsConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                    .allowedOrigins("http://localhost:3000", "https://myapp.com")
                    .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                    .allowedHeaders("*");
            }
        };
    }
}

If you’re using Spring Security, you also need to enable CORS in the security filter chain. Without this, Spring Security will block preflight requests before they reach your CORS configuration:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.cors(Customizer.withDefaults());
    // ... other security config
    return http.build();
}

Fix 5: ASP.NET Core

In Program.cs (or Startup.cs):

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy.WithOrigins("http://localhost:3000", "https://myapp.com")
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

app.UseCors(); // Must be after UseRouting but before UseAuthorization

app.MapControllers();
app.Run();

Fix 6: nginx

Add the headers in your server or location block:

location /api/ {
    # Handle preflight requests
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'http://localhost:3000';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }

    add_header 'Access-Control-Allow-Origin' 'http://localhost:3000' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;

    proxy_pass http://backend;
}

The always parameter ensures headers are added even on error responses (4xx, 5xx). Without it, nginx only adds headers on successful responses, and CORS errors on failed requests become confusing to debug.

Fix 7: Apache

Enable mod_headers and add to your .htaccess or virtual host config:

Header set Access-Control-Allow-Origin "http://localhost:3000"
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type, Authorization"

# Handle preflight
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]

Pro Tip: In Express, always place the cors() middleware before any route handlers or authentication middleware. If auth middleware rejects the preflight OPTIONS request (which carries no credentials), the browser sees a CORS error instead of a 401, making the problem nearly impossible to diagnose.

Fix 8: Development Proxy (No Backend Changes)

If you don’t control the backend or just want a quick dev setup, configure your frontend dev server to proxy API requests. The browser sees requests going to the same origin, so CORS doesn’t apply.

Vite

In vite.config.js:

import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: true
      }
    }
  }
});

Now fetch('/api/data') in your frontend code is proxied to http://localhost:5000/api/data.

Create React App

In package.json:

{
  "proxy": "http://localhost:5000"
}

Or for more control, install http-proxy-middleware and create src/setupProxy.js:

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
  app.use('/api', createProxyMiddleware({
    target: 'http://localhost:5000',
    changeOrigin: true
  }));
};

Next.js

In next.config.js:

module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'http://localhost:5000/api/:path*'
      }
    ];
  }
};

Note: Dev proxies only work during development. For production, you must configure CORS on the server or put both frontend and API behind the same domain using a reverse proxy.

Preflight Requests (OPTIONS)

Not all requests trigger a CORS preflight. Simple requests (GET/POST with basic headers and certain content types) go straight through. But if your request uses:

  • Methods like PUT, DELETE, or PATCH
  • A Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain
  • Custom headers like Authorization, X-Custom-Header, etc.

…the browser sends an OPTIONS request first (the “preflight”) to ask the server if the actual request is allowed. The server must respond with the appropriate Access-Control-Allow-* headers and a 2xx status code.

Common Preflight Mistakes

Your server returns 404 or 405 for OPTIONS requests. Many routers don’t have an OPTIONS handler by default. If the preflight gets a non-2xx response, the actual request is blocked.

Fix for Express (if not using the cors middleware):

app.options('*', (req, res) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.sendStatus(204);
});

Authentication middleware rejects the preflight. OPTIONS requests don’t carry your auth token. If your auth middleware runs before CORS handling, it blocks the preflight. Always handle CORS/OPTIONS before authentication.

Missing headers in Access-Control-Allow-Headers. If you send Authorization: Bearer ... but the server doesn’t include Authorization in its Access-Control-Allow-Headers response, the preflight fails. The error message will say:

Request header field Authorization is not allowed by Access-Control-Allow-Headers
in preflight response.

Add the header name to your allowed headers list.

Credentials and Cookies with CORS

If your request includes cookies or HTTP authentication, you need extra configuration on both sides.

Frontend — set credentials:

// fetch
fetch('https://api.example.com/data', {
  credentials: 'include'
});

// axios
axios.get('https://api.example.com/data', {
  withCredentials: true
});

Backend — add Access-Control-Allow-Credentials:

Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true

In Express with the cors middleware:

app.use(cors({
  origin: 'https://myapp.com',
  credentials: true
}));

The Wildcard Trap

You cannot use Access-Control-Allow-Origin: * with credentials. If you do, the browser blocks the response with:

Access to fetch at '...' from origin '...' has been blocked by CORS policy: The value
of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard
'*' when the request's credentials mode is 'include'.

You must specify the exact origin. If you need to allow multiple origins with credentials, read the origin from the request and reflect it back (after validating it against an allowlist):

const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];

app.use(cors({
  origin: function (origin, callback) {
    // !origin allows non-browser requests (e.g., curl, server-to-server)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, origin);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true
}));

The same restriction applies to Access-Control-Allow-Headers and Access-Control-Allow-Methods — you cannot use * for those when credentials are included. List each value explicitly.

Still Not Working?

If you’ve added the CORS headers and the error persists, check these less obvious causes:

  1. The request is being redirected. If your server redirects (e.g., http:// to https://, or adding/removing a trailing slash), the CORS headers may be on the final response but not on the redirect response itself. The browser treats the redirect as a new request and fails the CORS check. Fix the URL in your frontend code to match the final URL directly, avoiding the redirect.

  2. Mixed content. Your frontend is on https:// but the API request goes to http://. Browsers block mixed content regardless of CORS headers. Use HTTPS for both, or during development, run both on HTTP.

  3. The server error is masking the CORS error. If the server returns a 500 error and your error-handling middleware doesn’t set CORS headers on error responses, the browser shows a CORS error instead of the actual server error. In nginx, use the always flag on add_header. In Express, make sure your CORS middleware is before your route handlers so it runs even when a route throws.

  4. A browser extension is interfering. Extensions like ad blockers or privacy tools can strip or block CORS headers. Test in incognito mode with extensions disabled.

  5. The response is an opaque response from no-cors mode. If you set mode: 'no-cors' on your fetch request to “fix” the error, the request goes through but the response is opaque — you can’t read the body, status, or headers. This isn’t a fix. Remove mode: 'no-cors' and configure the server properly instead.

  6. The Origin header is not being sent. Some requests (same-origin, navigations) don’t include the Origin header. If your server conditionally sets Access-Control-Allow-Origin only when it sees the Origin header, it might not send the header on responses to requests that lack it. Make sure your CORS logic handles this case.

  7. A CDN or caching layer is stripping headers. If you have CloudFront, Cloudflare, or another CDN in front of your API, it may cache a response without CORS headers and serve that cached version to cross-origin requests. Add Vary: Origin to your responses so the CDN caches different versions for different origins. In CloudFront, you also need to whitelist the Origin header in your cache behavior settings.

  8. You’re calling a third-party API directly from the browser. Many APIs (payment processors, internal services) are not designed to be called from browsers and will never add CORS headers. Call them from your backend server instead, then have your frontend call your backend. Your backend-to-API request is server-to-server and is not subject to CORS.

  9. A service worker is intercepting the fetch. If your app registers a service worker, the worker can intercept fetch() calls and either rewrite them or replay them with different credentials. A service worker that calls event.respondWith(fetch(event.request)) re-issues the request, which may strip the Origin header in some browsers or change the credentials mode. Unregister the service worker temporarily (in DevTools → Application → Service Workers) to confirm whether it is the source of the error.

  10. The CORS preflight is timing out, not failing. If your server is slow to respond to OPTIONS (for example, because it runs an expensive auth lookup), the browser may abort the preflight and report a generic CORS failure. Look at the Network tab for an OPTIONS request that stays “pending” past 10–30 seconds. Cache preflight results on the client side by returning Access-Control-Max-Age: 86400 from the server so the browser does not re-preflight every request.

  11. You renamed a custom header and forgot to update Access-Control-Allow-Headers. Changing X-Auth-Token to X-API-Key on the frontend without updating the server’s allow-list causes the preflight to silently reject. The browser console message names the offending header — read it carefully and update the server.

  12. The framework is sending the response before middleware runs. In Next.js API routes and some serverless deployments, if you return from the handler before the CORS middleware adds headers, the response goes out without them. Use the framework’s recommended CORS pattern instead of trying to set headers manually inside the handler. For Next.js specifically, see Fix: Next.js CORS error.


Related: If your development server won’t start because the port is already taken, see Fix: Port 3000 Is Already in Use. If the CORS error specifically mentions the preflight, see Fix: CORS preflight request blocked.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles