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

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.

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]

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.


Related: If your development server won’t start because the port is already taken, see Fix: Port 3000 Is Already in Use.

Related Articles