Skip to content
api2026-07-255 min read

You call an API from the browser. The URL is fine, the parameters are correct, but the console throws a red error:

Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check: No 'Access-Control-Allow-Origin' header is present.

What's more confusing: you open the Network panel and find an OPTIONS request you never explicitly sent. That OPTIONS request is the so-called CORS preflight request.

When Does a Preflight Request Get Triggered?

The browser's same-origin policy dictates that a page can only request resources with the same protocol, domain, and port. CORS (Cross-Origin Resource Sharing) is the server-side mechanism that "authorizes" cross-origin access. A preflight request is the browser asking the server — before sending a "potentially side-effecting" request — "Can I make this kind of request to you?"

Simple requests do not trigger a preflight. They must meet all of these:

  • The method is GET, HEAD, or POST
  • No custom request headers (only safe headers like Accept, Accept-Language, Content-Language, Content-Type)
  • Content-Type is only text/plain, multipart/form-data, or application/x-www-form-urlencoded

Any of the following triggers a preflight:

  • Using non-simple methods like PUT, DELETE, PATCH
  • Adding custom request headers, e.g. Authorization: Bearer xxx
  • Setting Content-Type to application/json (the most common trap for frontend devs)
// This triggers a preflight because Content-Type is application/json
fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'test' })
});

// This one too, because it uses DELETE
fetch('https://api.example.com/users/1', {
  method: 'DELETE'
});

// And this one, because of the custom header
fetch('https://api.example.com/data', {
  headers: { 'X-API-Key': 'abc123' }
});

What Does the Preflight Actually Send? How Do You Read the Response?

When a preflight is triggered, the browser automatically sends an OPTIONS request with these key headers:

OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
  • Origin: the origin making the request (your frontend address)
  • Access-Control-Request-Method: the method the actual request will use
  • Access-Control-Request-Headers: the custom headers the actual request will send

The server must "approve" these conditions in its response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

What each response header means:

| Response Header | Purpose | |--------|------| | Access-Control-Allow-Origin | Which origins are allowed. * means all origins (but cannot be used with credentials) | | Access-Control-Allow-Methods | The list of allowed HTTP methods | | Access-Control-Allow-Headers | The allowed custom request headers | | Access-Control-Max-Age | How long (seconds) to cache the preflight result, to avoid sending OPTIONS on every request | | Access-Control-Allow-Credentials | Whether cookies can be sent (when set to true, Origin cannot be *) |

If the server's response headers don't satisfy the browser's checks, the actual request is never sent — and you see that CORS error.

Common CORS Errors and How to Fix Them

Error 1: No 'Access-Control-Allow-Origin' header

The server has no CORS configuration at all. Add the response headers on the server:

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

// Or just use the cors middleware
const cors = require('cors');
app.use(cors({ origin: 'http://localhost:3000' }));

Error 2: Method not allowed in preflight

The server's CORS config doesn't include the HTTP method you're using. For example, you send PUT but Access-Control-Allow-Methods only lists GET, POST.

Error 3: Request header field X is not allowed

A custom request header wasn't approved by Access-Control-Allow-Headers. Check that every non-standard header you send is in the server's allow list.

Error 4: credentials mode is 'include' but Allow-Origin is '*'

When fetch is configured with credentials: 'include' (to send cookies), the server cannot use Access-Control-Allow-Origin: *. It must specify the exact Origin and add Access-Control-Allow-Credentials: true.

Why Local API Testing Tools Aren't Subject to CORS

Here's a key insight: CORS is a browser security mechanism, not an HTTP protocol restriction.

  • fetch and XMLHttpRequest in the browser are subject to the same-origin policy
  • Requests made by CLI tools (curl, httpie), desktop apps, and backend services are completely unaffected by CORS

This is why hitting an endpoint with Postman or curl works fine, but switching to the browser gives you a CORS error.

For frontend developers who hit CORS issues constantly during development, a practical approach is to use an API testing tool that doesn't go through the browser. The API Tester makes HTTP requests directly from your local machine, bypassing the browser sandbox entirely, so it never triggers preflight requests or CORS checks. You can focus on whether the endpoint's logic is correct without being distracted by cross-origin configuration.

Of course, in production you still need to configure CORS correctly — because end users access your site through a browser. But during development and debugging, separating "endpoint logic issues" from "CORS configuration issues" makes troubleshooting far more efficient.

Temporary Workarounds During Development

If you don't have permission to change the server's CORS config, during development you can:

  1. Frontend proxy: Configure a proxy in vite.config.js or webpack devServer so the dev server forwards requests on your behalf
  2. Browser extension: Temporarily disable CORS checks (dev environment only — never use for daily browsing)
  3. Local API tool: Use the API Tester to hit the endpoint directly, bypassing browser restrictions
// Vite proxy config example
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

The core of understanding CORS preflight in one sentence: before sending a "non-simple request," the browser sends an OPTIONS request to ask the server's permission, and the server responds via the Access-Control-* series of headers. Once you understand the trigger conditions and how the response headers map to them, the vast majority of CORS issues can be located and fixed in minutes.

Next time you hit a CORS error, open the Network panel, find that OPTIONS request, and compare the Access-Control-Request-* values with the Access-Control-Allow-* values — the answer is usually right there. If you just want to confirm the endpoint itself works, fire a request with the API Tester — local processing, no CORS headaches, and no worry about request data passing through a third-party server.


ad