CORS policy: No ‘Access-Control-Allow-Origin’ header is present

Quick answer

The browser blocked your cross-origin request because the API server did not include the Access-Control-Allow-Origin header in its response. The fix is on the server: add CORS headers. You cannot bypass this from the browser side.

The exact error string

The full error message shown in the browser console looks like this:

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

It is always accompanied by a TypeError: Failed to fetch (Chrome/Edge), TypeError: NetworkError when attempting to fetch resource (Firefox), or TypeError: Load failed (Safari) thrown by the fetch promise itself. The CORS message is the reason; the TypeError is the JavaScript error.

Browser-specific error messages

Every major browser surfaces the CORS block differently. Knowing which variant to search for speeds up debugging.

Chrome / Edge

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

TypeError: Failed to fetch

Chrome and Edge show the most detailed message — they tell you the exact requesting origin and target URL. The CORS line appears as a console error; the TypeError: Failed to fetch is the uncaught Promise rejection your code sees.

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.

TypeError: NetworkError when attempting to fetch resource.

Firefox shows the status code of the server response (often 200), which is a useful reminder that the server did respond — CORS is blocking the browser from reading it, not the server from sending it.

Safari

Fetch API cannot load https://api.example.com/data.
Origin https://myapp.com is not allowed by Access-Control-Allow-Origin.

TypeError: Load failed

Safari gives the least information but the same root cause. The TypeError: Load failed error will be what your .catch() handler receives. You can confirm it is CORS by checking the Console (not just the Network tab) for the additional CORS line.

How to spot it in DevTools

Regardless of browser, open DevTools → Network tab → click the failing request. In the Response Headers section you will see no Access-Control-Allow-Origin header. That absence is the definitive confirmation. The Console tab will then show the browser-specific CORS message above.

Why CORS exists

CORS (Cross-Origin Resource Sharing) is a browser security mechanism. By default, browsers block JavaScript from reading responses from a different origin (a different domain, subdomain, port, or protocol) unless the server explicitly opts in by including specific HTTP response headers. Without CORS, a malicious website could make authenticated API requests on behalf of a logged-in user and read the results — a type of attack called CSRF (Cross-Site Request Forgery).

Critically: CORS is enforced by the browser, not by the server. The server may have processed the request successfully and returned a 200 OK response with data — but the browser reads the CORS headers, finds Access-Control-Allow-Origin absent, and discards the response before your JavaScript ever sees it. This is why the same request works with curl (no CORS enforcement) but fails in the browser.

Common causes

Fix: Express (Node.js)

Install the cors package and add it as middleware before your routes:

// npm install cors
const express = require('express');
const cors = require('cors');
const app = express();

// Allow one specific origin (production)
app.use(cors({ origin: 'https://myapp.com' }));

// Allow multiple origins
app.use(cors({
  origin: ['https://myapp.com', 'https://staging.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// Allow all origins (development only — not for production)
app.use(cors());

app.get('/data', (req, res) => res.json({ hello: 'world' }));

Fix: Flask (Python)

# pip install flask-cors
from flask import Flask, jsonify
from flask_cors import CORS

app = Flask(__name__)

# Allow all origins (development)
CORS(app)

# Allow a specific origin (production)
CORS(app, origins=["https://myapp.com"])

@app.route('/data')
def data():
    return jsonify(hello='world')

Fix: FastAPI (Python)

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://myapp.com"],  # or ["*"] for development
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/data")
def data():
    return {"hello": "world"}

Fix: Spring Boot (Java)

// Option 1: annotation per controller or method
@CrossOrigin(origins = "https://myapp.com")
@RestController
public class DataController {
    @GetMapping("/data")
    public Map<String, String> data() {
        return Map.of("hello", "world");
    }
}

// Option 2: global CORS config (preferred)
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://myapp.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE")
            .allowedHeaders("*");
    }
}

Fix: NGINX reverse proxy

If you are using NGINX in front of your API server and CORS headers are being stripped or duplicated, you can add them directly in the NGINX config:

server {
    listen 443 ssl;
    server_name api.example.com;

    location /api/ {
        proxy_pass http://localhost:3000;

        # Add CORS headers for all responses
        add_header Access-Control-Allow-Origin "https://myapp.com" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;

        # Handle preflight OPTIONS request
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin "https://myapp.com";
            add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
            add_header Access-Control-Allow-Headers "Authorization, Content-Type";
            add_header Content-Length 0;
            return 204;
        }
    }
}

The preflight request (OPTIONS)

For "non-simple" requests — those using methods other than GET/POST, or custom headers like Authorization or Content-Type: application/json — the browser automatically sends an OPTIONS preflight request before the actual request. The server must respond to the OPTIONS request with the correct CORS headers and a 200 or 204 status, or the browser will block the actual request.

A common bug is handling GET requests correctly but not OPTIONS. If you add CORS headers only in your route handlers (not in a middleware that runs for all methods), OPTIONS requests will return 404 or no CORS headers, and the real request will never be sent.

// Test the preflight manually with curl
curl -v -X OPTIONS https://api.example.com/data \
  -H "Origin: https://myapp.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization"

# You should see these response headers:
# Access-Control-Allow-Origin: https://myapp.com
# Access-Control-Allow-Methods: POST
# Access-Control-Allow-Headers: Content-Type, Authorization
# HTTP/1.1 200 OK (or 204)

Credentials and CORS: the wildcard restriction

When your fetch request sends cookies or HTTP auth credentials (using credentials: 'include'), the server cannot use a wildcard * for Access-Control-Allow-Origin. The browser requires an explicit, matching origin and also requires the server to return Access-Control-Allow-Credentials: true:

// Client: fetch with credentials
fetch('https://api.example.com/profile', {
  credentials: 'include' // sends cookies
});

// Server response headers required:
// Access-Control-Allow-Origin: https://myapp.com  (exact origin, not *)
// Access-Control-Allow-Credentials: true

// This will NOT work with credentials:
// Access-Control-Allow-Origin: *  (wildcard is rejected when credentials: 'include')

Development proxy: avoiding CORS during local development

The cleanest way to avoid CORS errors during local development is to configure a proxy in your frontend build tool. The proxy makes the API call from the dev server's own origin, so it is not a cross-origin request and CORS headers are irrelevant. This means you do not need to open up your API with permissive CORS rules in development.

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true
      }
    }
  }
}

// Now fetch('/api/data') in the browser goes through the Vite dev server proxy
// and is NOT a cross-origin request — no CORS headers needed

Debugging checklist

Frequently Asked Questions

What does "No Access-Control-Allow-Origin header is present" mean?

It means the API server did not include the Access-Control-Allow-Origin header in its HTTP response. When a browser script makes a cross-origin request, it checks this header to decide whether to allow the response. If the header is absent, the browser discards the response and throws a CORS error — even if the server successfully processed the request and returned data.

Is the CORS error a server problem or a client problem?

The CORS error is always fixed on the server. The browser enforces the CORS policy as a security feature, but it is the server's responsibility to declare which origins it allows by returning the Access-Control-Allow-Origin header. You cannot bypass CORS from the browser side — you must configure the server (or use a development proxy).

Why does the CORS error only happen in the browser but not with curl?

CORS is a browser-enforced security policy. curl and other server-to-server HTTP clients do not enforce CORS — they send and receive headers as-is. The browser discards cross-origin responses that lack the Access-Control-Allow-Origin header; curl does not. This is why the same request succeeds in curl but fails in the browser.

Can I use Access-Control-Allow-Origin: * with credentials?

No. When a fetch request includes credentials (cookies, HTTP auth, or client certificates) via credentials: 'include', the server must return a specific origin in Access-Control-Allow-Origin (not a wildcard *), and must also return Access-Control-Allow-Credentials: true. Using * with credentials will cause the browser to reject the response.

What is a CORS preflight request?

A preflight is an automatic OPTIONS request the browser sends before certain cross-origin requests — specifically those with non-simple methods (PUT, DELETE, PATCH) or custom headers like Authorization. The server must respond to the OPTIONS request with the correct CORS headers before the browser will send the actual request. Many CORS bugs happen because the server handles GET correctly but ignores OPTIONS.

How do I fix CORS in a development environment?

The best approach in development is to configure a proxy in your frontend build tool (Vite, Webpack, Create React App). The proxy makes the API call from the same origin as the dev server, so CORS headers are not needed. This avoids having to open up your API with permissive CORS rules during development while keeping the production configuration strict.

Working with a JSON API?

Format and validate any JSON response in your browser — nothing is uploaded to a server.

JSON Formatter JSON Validator All Error References
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.