The Shack Web Development What Is CORS and How Do You Fix It?

What Is CORS and How Do You Fix It? A Plain-English Guide

Back to All Posts

You're testing your frontend. Everything looks fine. You open the browser console and there it is: "Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy." The request never made it. Your network tab shows nothing useful. And the error message tells you what happened but not why, or more usefully, how to fix it.

CORS (Cross-Origin Resource Sharing) is one of the most reliably confusing things developers encounter, mostly because it operates at the browser level and the error messages are deliberately vague for security reasons. This guide explains exactly what CORS is, why it exists, how the handshake works, and how to fix every common variant of the error.

The Same-Origin Policy — Why CORS Exists

Before CORS, there was the Same-Origin Policy (SOP), a browser security rule that has been in place since the late 1990s. The rule is simple: JavaScript running on one origin cannot read responses from a different origin.

An origin is the combination of three things: scheme (protocol), hostname, and port. All three must match for two URLs to be considered the same origin:

https://example.com       ← origin A
https://example.com/page  ← same origin as A (path doesn't matter)
http://example.com        ← different origin (different scheme)
https://api.example.com   ← different origin (different subdomain)
https://example.com:8080  ← different origin (different port)
https://other.com         ← different origin (different hostname)

The Same-Origin Policy exists because without it, any malicious website you visit could use JavaScript to make authenticated requests to your bank, your email provider, or any other site you're logged into, and read the responses. Your session cookies would be sent along automatically, and the attacker could silently harvest whatever the server returned.

CORS is not a restriction added on top of the web; it's a controlled relaxation of the Same-Origin Policy. It gives servers a way to say "I'm happy to serve responses to requests from this other origin," which the browser then honours.

How CORS Actually Works

CORS is entirely a browser mechanism. The server doesn't enforce it; the browser does. When your JavaScript makes a cross-origin request, the browser checks the response headers to decide whether to hand the response back to your code. If the right headers aren't present, the browser blocks the response, even though the server sent one. This is why you can sometimes see a 200 OK in the server logs while the browser throws a CORS error. The request reached the server just fine; the browser simply refused to let your code see the reply.

Simple Requests

Some cross-origin requests are considered "simple" and go through without a preflight check. A request is simple when it uses GET, HEAD, or POST; when it only uses safe headers (like Accept, Content-Type with specific values, Accept-Language); and when the Content-Type, if set, is one of application/x-www-form-urlencoded, multipart/form-data, or text/plain.

For simple requests, the browser sends the request directly with an Origin header, and checks the response for Access-Control-Allow-Origin. If that header is present and matches the requesting origin (or is *), the browser passes the response through. If it's absent or doesn't match, the browser blocks it.

Preflight Requests

Anything that doesn't qualify as simple, such as a PUT or DELETE request, a POST with application/json, or any custom header like Authorization or X-API-Key, triggers a preflight. Before sending the actual request, the browser sends an OPTIONS request to the same URL, asking the server: "Would you be willing to accept a request with these characteristics from this origin?"

OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

The server must respond with the appropriate CORS headers confirming what it will allow:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

If that preflight succeeds, the browser sends the actual request. If the preflight fails for any reason, whether a wrong origin, missing header, or method not listed, the browser blocks everything and you get the CORS error. The actual request never goes out.

The CORS Response Headers Explained

Access-Control-Allow-Origin — The most important header. Specifies which origin(s) are allowed. Can be a specific origin (https://myapp.com), a wildcard (*), or dynamically set per-request. You cannot list multiple origins here directly. If you need to support several origins, you must reflect the requesting origin conditionally in your server code.

Access-Control-Allow-Methods — Which HTTP methods are permitted for cross-origin requests. If your API accepts DELETE but this header only lists GET and POST, preflight will fail for DELETE requests.

Access-Control-Allow-Headers — Which request headers are allowed. If you're sending Authorization or a custom header and it's not listed here, the preflight fails. This header must explicitly list every custom header your client sends.

Access-Control-Allow-Credentials — Set to true to allow the browser to send cookies and HTTP auth headers with cross-origin requests. When this is true, Access-Control-Allow-Origin cannot be *; it must be a specific origin.

Access-Control-Max-Age — How long (in seconds) the browser can cache a preflight response. Setting this to a high value (like 86400 for 24 hours) reduces the number of preflight requests in production and improves performance.

Access-Control-Expose-Headers — By default, JavaScript can only read a small set of "safe" response headers. If you want your client code to access custom response headers (like a rate-limit header or a pagination cursor), you must list them here.

Fixing CORS: Server-Side Solutions

The right fix is always on the server. CORS errors mean the server hasn't told the browser it's allowed to share the response, so the fix is configuring the server to send the right headers.

Apache (.htaccess)

<IfModule mod_headers.c>
  Header set Access-Control-Allow-Origin "https://myapp.com"
  Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
  Header set Access-Control-Allow-Headers "Content-Type, Authorization"
</IfModule>

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

nginx

location /api/ {
  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 "Content-Type, Authorization";

  if ($request_method = OPTIONS) {
    add_header Access-Control-Max-Age 86400;
    return 204;
  }
}

Node.js / Express

const cors = require('cors');

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

// Handle preflight for all routes
app.options('*', cors());

PHP

header("Access-Control-Allow-Origin: https://myapp.com");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
  http_response_code(204);
  exit();
}
Inspect headers instantly: Open your browser's DevTools, go to the Network tab, click any request, and check the Response Headers panel. You'll see exactly which CORS headers the server sent — or didn't send — for that request.

Supporting Multiple Origins

Access-Control-Allow-Origin only accepts a single value; you can't write https://app.com, https://staging.app.com. To allow multiple specific origins, reflect the requesting origin conditionally in your server code:

// Node.js / Express example
const allowedOrigins = [
  'https://myapp.com',
  'https://staging.myapp.com',
  'http://localhost:3000'
];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  next();
});

This pattern is the standard approach for production APIs that serve multiple frontend environments. The wildcard * is convenient for truly public APIs with no authentication, but should never be used alongside Access-Control-Allow-Credentials: true.

Common Mistakes and Misunderstandings

"I'll just use * for everything." This works for unauthenticated public APIs, but breaks the moment you need to send cookies or an Authorization header. When credentials are involved, you must specify the exact origin.

Setting CORS headers on the frontend. A surprisingly common mistake. Your JavaScript code cannot set Access-Control-Allow-Origin; that's a response header set by the server. Headers set by JavaScript are request headers. The browser sends them to the server; they don't bypass CORS.

Forgetting to handle OPTIONS. If your server returns a 404 or 405 for OPTIONS requests (because your router doesn't have a route for it), every preflight will fail. Make sure your server responds to OPTIONS with a 200 or 204 and the appropriate CORS headers.

The request works in Postman but not the browser. Postman doesn't enforce CORS; it's not a browser. CORS is entirely a browser security mechanism. If a request works in Postman but fails in the browser, that confirms it's a CORS configuration issue on the server, not a network or authentication problem.

Caching a bad preflight response. Browsers cache preflight responses. If you fix your CORS headers on the server but the browser cached a failed preflight (some browsers do cache failures), you may still see errors. Hard-refresh with cache cleared, or use a private window to test immediately after a fix.

Development Workarounds (Not for Production)

When you're developing locally and don't control the API server, a few options can unblock you while you wait for a proper fix:

A local proxy. Configure your dev server (Vite, webpack-dev-server, Create React App) to proxy requests through itself. The browser sees a same-origin request; the dev server forwards it to the API. Since the proxy is server-to-server, CORS doesn't apply.

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

A CORS proxy service. Public CORS proxies (like cors-anywhere) can be self-hosted for development. Avoid relying on public instances in anything resembling production, as you'd be routing your traffic through a third party.

Never disable browser CORS in production. Browser extensions or flags that disable CORS enforcement exist for developer convenience only. Shipping with CORS disabled, or advising users to disable it, removes a core browser security boundary. The right fix is always configuring the server correctly.

CORS and the Security Model

It's worth understanding what CORS protects against, and what it doesn't. CORS prevents a malicious page from reading cross-origin responses. It doesn't prevent the request from being sent at all (for simple requests); it just stops JavaScript from reading the response.

CORS also doesn't protect against Cross-Site Request Forgery (CSRF) in the general case. A form POST from a third-party site doesn't trigger a preflight and doesn't require CORS headers to succeed. The request goes through; the browser just won't let any co-located JavaScript read the response. For CSRF protection you still need tokens, SameSite cookies, or other server-side validation.

Understanding this distinction matters when you're evaluating security posture. CORS is one layer of browser-enforced protection; it works in concert with SameSite cookies, Content Security Policy, and server-side auth checks, not as a replacement for them.


CORS errors feel mysterious until you understand that the browser is enforcing a deliberate security contract between your frontend and backend. Once that clicks, diagnosing and fixing them becomes a straightforward header configuration exercise. Check the preflight, check the response headers, confirm your origin is exactly right (scheme, hostname, port), and ensure OPTIONS requests get a proper response. That covers the vast majority of CORS issues you'll ever encounter.