A CORS error can look like an API outage, an authentication problem, or a broken frontend. In reality, what does a CORS error mean is usually much narrower: the browser blocked frontend JavaScript from reading a response from a different origin because the server’s cross-origin policy did not allow it.

CORS is one of the most commonly misunderstood browser security mechanisms because the network request may still reach the server, the endpoint may work in curl or Postman, and the browser console may hide the response details. This guide explains what is happening, how to identify the exact failure, and how to fix CORS without exposing API keys or weakening your application’s security model.

CORS means Cross-Origin Resource Sharing

Cross-Origin Resource Sharing (CORS) is an HTTP-based permission system enforced by web browsers. It lets a server tell a browser which other websites are allowed to read that server’s responses through JavaScript APIs such as fetch() and XMLHttpRequest.

An origin is the combination of:

  • Scheme or protocol, such as https
  • Hostname, such as app.example.com
  • Port, such as 443 or 3000

That means each of these is a different origin:

https://app.example.com
http://app.example.com
https://api.example.com
https://app.example.com:3000

Even though the hostnames may look related, a browser treats them as separate origins. A page loaded from https://app.example.com making a request to https://api.example.com/v1/orders is making a cross-origin request.

The browser’s same-origin policy normally prevents JavaScript on one origin from freely reading data from another origin. CORS is the controlled opt-in mechanism that relaxes that restriction when the receiving server explicitly permits it. The Fetch standard defines the browser behavior behind this protocol, including CORS checks and preflight requests. (fetch.spec.whatwg.org)

What a CORS error actually means

A CORS error does not automatically mean that:

  • Your API server is down.
  • DNS is misconfigured.
  • SMTP is unavailable.
  • The user’s internet connection failed.
  • Your API key is invalid.
  • The API did not receive the request.

It means the browser did not receive the CORS permissions it needed to expose the response to your page’s JavaScript.

A typical browser error looks like this:

Access to fetch at 'https://api.example.com/v1/messages'
from origin 'https://app.example.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check.

Or:

No 'Access-Control-Allow-Origin' header is present on the requested resource.

The essential detail is the phrase “from origin.” The browser tells you which website made the request. Your API must either return an Access-Control-Allow-Origin value matching that site or return * when the endpoint is intentionally public and does not use credentials.

For example, if the request comes from:

Origin: https://app.example.com

a server can allow it with:

Access-Control-Allow-Origin: https://app.example.com

The browser compares the two values exactly. https://app.example.com does not match http://app.example.com, https://www.example.com, or https://app.example.com:3000.

CORS is a browser restriction, not an API authentication system

One reason CORS is confusing is that it applies to browser JavaScript, not to HTTP clients generally.

This command can work:

curl -i https://api.example.com/v1/messages \
  -H 'Authorization: Bearer example-token'

The same endpoint can fail when called from browser JavaScript:

fetch("https://api.example.com/v1/messages", {
  headers: {
    Authorization: "Bearer example-token"
  }
});

That difference does not prove curl is “bypassing” the API. Curl is not a browser and does not enforce the same-origin policy. Postman, Insomnia, server-side Node.js code, backend jobs, and most API testing tools likewise do not block a response based on CORS.

This is why “it works in Postman” is a useful diagnostic clue. It often means:

  1. The endpoint is reachable.
  2. The method, URL, and credentials may be valid.
  3. The remaining problem is likely browser-specific CORS behavior.

CORS should also never be treated as access control. A malicious script cannot read a protected cross-origin response in a browser without permission, but a non-browser client can still send requests to a public endpoint. Authentication, authorization, rate limits, CSRF defenses where appropriate, and input validation remain necessary.

How the browser decides whether to allow a request

A browser sends an Origin request header on relevant cross-origin requests. The target server then returns response headers that state whether the browser may expose the response to the calling script.

The most important response header is:

Access-Control-Allow-Origin: https://app.example.com

For a deliberately public resource, such as an unauthenticated public configuration endpoint, the server may instead use:

Access-Control-Allow-Origin: *

That wildcard does not mean every request is automatically safe. It means any website can ask a browser to read the response. It is generally appropriate only when the response is genuinely public and the request does not rely on browser credentials.

Simple requests versus preflighted requests

Some cross-origin requests can proceed directly. Others require the browser to first send a preflight request using the OPTIONS method.

A preflight is likely when your frontend sends a request such as:

fetch("https://api.example.com/v1/messages", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer example-token"
  },
  body: JSON.stringify({ to: "customer@example.net" })
});

The Authorization header and JSON content type commonly cause the browser to preflight. Before it sends the POST, the browser asks the server whether that origin, method, and header combination is allowed.

A representative preflight request is:

OPTIONS /v1/messages HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type

A successful response might be:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Vary: Origin

The browser then sends the actual POST. If the OPTIONS response omits a required permission, returns a redirect, returns an error, or is intercepted by a proxy, the browser blocks the real request or blocks JavaScript from using the result. CORS preflight requests exist specifically to let the server evaluate the intended method and request headers before the browser proceeds. (developer.mozilla.org)

The most common CORS error messages and their causes

Browser wording varies, but the failure usually falls into a small set of categories.

Missing Access-Control-Allow-Origin

This is the classic error:

No 'Access-Control-Allow-Origin' header is present on the requested resource.

The server response did not include an allowed origin. Possible causes include:

  • CORS middleware was never enabled.
  • CORS is enabled on one route but not another.
  • Your application returns CORS headers on successful responses but not on errors.
  • A reverse proxy, CDN, load balancer, or authentication layer generated the response before your app did.
  • The configured origin is slightly different from the browser origin.

The fix is on the server or gateway serving the API. Adding mode: "cors" to client-side fetch() does not grant permission; the server must send the response header.

Preflight request does not pass access control check

This means the browser sent OPTIONS first and the server did not approve the requested method or headers.

Common causes include:

Access-Control-Allow-Methods: GET

when the frontend is trying to send POST, or:

Access-Control-Allow-Headers: Content-Type

when the browser requested both authorization and content-type.

The server must include all methods and non-safelisted request headers that the browser requested. Header names are case-insensitive in HTTP, but inspecting the exact values in DevTools avoids configuration mistakes.

Wildcard origin with credentials

A frequent error is equivalent to:

Credential is not supported if the CORS header
'Access-Control-Allow-Origin' is '*'.

When a request includes browser credentials, the server cannot combine:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Instead, it must return a specific allowed origin:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

The credential mode can be triggered by cookies, HTTP authentication, or a client setting such as:

fetch(url, { credentials: "include" });

A wildcard origin is incompatible with credentialed CORS requests. (developer.mozilla.org)

Redirects during CORS or preflight

An API may redirect http to https, add a trailing slash, redirect to a login page, or redirect across domains. Redirect behavior can complicate CORS, especially for preflight requests.

For example, an unauthenticated OPTIONS request might be redirected to:

https://app.example.com/login

That is not a valid CORS approval response for an API call. Ensure that OPTIONS reaches a CORS handler before session-based authentication redirects, and call the final HTTPS API URL directly from the frontend.

CORS headers missing on error responses

A 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests, or 500 Internal Server Error can turn into an unhelpful CORS message if the error response lacks CORS headers.

This creates two problems: the user cannot see the actual API error in JavaScript, and the developer may spend time debugging CORS when the underlying problem is authorization or an invalid request.

Apply CORS headers consistently to successful responses, validation failures, rate-limit responses, and server-generated errors. The HTTP status code is still important: 401 indicates missing or invalid authentication credentials, while 403 means the request is understood but forbidden. The IANA HTTP status code registry maintains the standard status code definitions. (iana.org)

A practical CORS debugging workflow

Do not guess based only on the console error. Use the browser network panel to inspect the actual request chain.

1. Identify the frontend origin exactly

Open the page in the browser and note the complete origin:

https://app.example.com

During local development, it may instead be:

http://localhost:5173

These are not interchangeable. The port matters. A local frontend on http://localhost:3000 is different from http://localhost:5173.

2. Inspect the OPTIONS request

In browser DevTools, open Network, reload the page, and look for an OPTIONS request immediately before the failed API request.

Check:

  • Was an OPTIONS request sent?
  • What status code did it return?
  • Did it redirect?
  • Does the response include Access-Control-Allow-Origin?
  • Does Access-Control-Allow-Methods include the intended method?
  • Does Access-Control-Allow-Headers include the requested headers?
  • Is a CDN, WAF, proxy, or login middleware answering instead of the API?

A 204 No Content or 200 OK can both be valid preflight responses if the required CORS headers are present. A 301, 302, 401, 403, 404, 405, or 500 needs investigation.

3. Reproduce the preflight with curl

Use curl to send the same CORS negotiation manually:

curl -i -X OPTIONS 'https://api.example.com/v1/messages' \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization, content-type'

Look for a response like:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type

If curl returns a response without those headers, the server configuration is incomplete. If curl shows the correct headers but the browser still fails, compare the browser’s exact origin, request headers, redirect path, and cached response behavior.

4. Test the real API response separately

Then test the intended request directly from a trusted server-side environment. This helps distinguish CORS from ordinary API errors:

curl -i 'https://api.example.com/v1/messages' \
  -X POST \
  -H 'Authorization: Bearer example-token' \
  -H 'Content-Type: application/json' \
  --data '{"to":"customer@example.net","subject":"Test","text":"Hello"}'

If this returns 401, 403, or 422, solve that API problem too. CORS is only the browser’s permission layer; it does not validate your payload or authorize an account.

Safe server-side CORS patterns

The safest configuration is usually an explicit allowlist of trusted frontend origins.

For a single production frontend:

Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

For a development and production frontend, validate the request origin against a fixed allowlist before returning it:

https://app.example.com
https://staging.example.com
http://localhost:5173

Do not blindly copy every incoming Origin header into Access-Control-Allow-Origin. That turns an allowlist into an open policy and may expose responses to untrusted websites.

Example with Node.js HTTP

The following example shows the core behavior without relying on framework-specific middleware names:

import http from "node:http";

const allowedOrigins = new Set([
  "https://app.example.com",
  "https://staging.example.com",
  "http://localhost:5173"
]);

function setCorsHeaders(req, res) {
  const origin = req.headers.origin;

  if (origin && allowedOrigins.has(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
    res.setHeader("Vary", "Origin");
  }

  res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-ID");
}

http.createServer((req, res) => {
  setCorsHeaders(req, res);

  if (req.method === "OPTIONS") {
    res.writeHead(204);
    res.end();
    return;
  }

  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ ok: true }));
}).listen(8080);

This approach has three important properties: it checks the exact origin, it provides a valid preflight response, and it returns CORS headers before ordinary application responses are written.

Example with Nginx for one known frontend

For a single public frontend origin, an Nginx location can return fixed CORS headers:

location /v1/ {
    add_header Access-Control-Allow-Origin "https://app.example.com" always;
    add_header Access-Control-Allow-Methods "GET, POST, PATCH, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Request-ID" always;
    add_header Vary "Origin" always;

    if ($request_method = OPTIONS) {
        return 204;
    }

    proxy_pass http://application_upstream;
}

Use configuration appropriate to your infrastructure, and test both success and failure responses. In production, CORS headers may be set in the application, an API gateway, a reverse proxy, or a CDN edge layer. Choose one clear ownership point when possible so policies do not conflict.

Why Vary: Origin matters when origins are dynamic

If your server returns different Access-Control-Allow-Origin values depending on the incoming Origin, it should also send:

Vary: Origin

This tells caches that the response varies by requesting origin. Without it, a CDN or shared cache could store a response generated for one origin and serve it to another origin with the wrong CORS header.

For example, suppose the server receives:

Origin: https://app.example.com

and returns:

Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

A later request from https://staging.example.com should be evaluated independently. MDN specifically recommends Vary: Origin when the server dynamically sets Access-Control-Allow-Origin from the request origin. (developer.mozilla.org)

CORS and transactional email APIs

CORS becomes especially important when developers connect a browser application directly to a transactional email API.

The tempting architecture is:

Browser frontend → email provider REST API → recipient inbox

That is usually the wrong architecture for production email sending because the browser would need access to an API key capable of sending email. Any secret embedded in JavaScript, a mobile web bundle, browser storage, or a publicly reachable frontend request can be copied and abused.

The safer architecture is:

Browser frontend → your backend or serverless function → email API or SMTP relay

Your backend authenticates the user, validates the input, applies business rules and rate limits, and uses the email provider credential only on the server. This design often eliminates the CORS issue entirely because the browser calls an endpoint on your own origin, while server-to-server calls are not governed by browser CORS rules.

A REST email API and an SMTP relay are both typically server-side integrations. SMTP is not called through browser fetch(), and an SMTP delivery problem is not a CORS error. SMTP errors use different protocol codes, such as 535 for authentication failure, 550 for a mailbox or policy rejection, and 554 for a transaction failure.

If you are building a backend mail integration, use the provider’s email API reference and setup guides for the server-side request format, authentication method, and domain-verification workflow. Keep provider API tokens out of client-side code even if an API advertises broad CORS support.

CORS is not a DNS, SPF, DKIM, or DMARC problem

A CORS error occurs at the HTTP/browser layer. It is not fixed by adding a DNS record.

This distinction matters for email applications because developers often troubleshoot several separate systems at once:

Problem areaTypical symptomRelevant configuration
CORSBrowser blocks JavaScript from reading an API responseHTTP response headers and preflight handling
DNS resolutionHostname does not resolveA, AAAA, CNAME, or other DNS records
SPFReceiving server evaluates sender authorizationTXT record such as v=spf1 include:spf.example-mail.net -all
DKIMEmail signature cannot be validatedSelector-based TXT or CNAME record such as s1._domainkey.example.com
DMARCDomain owner publishes alignment policyTXT record at _dmarc.example.com
SMTPMail submission or relay failsSMTP host, port, TLS, username, password, and sender policy

For example, this SPF syntax is a DNS TXT record example:

example.com. IN TXT "v=spf1 include:spf.example-mail.net -all"

And this is the shape of a DKIM CNAME delegation record:

s1._domainkey.example.com. IN CNAME s1.domainkey.provider.example.

Those records can affect email authentication and deliverability, but they cannot add Access-Control-Allow-Origin to an HTTP response. Conversely, adding CORS headers cannot repair an SPF, DKIM, DMARC, MX, or SMTP configuration issue.

For email-domain troubleshooting, tools such as MXToolbox and mail-tester.com can help inspect DNS and message authentication. For a suspected CORS problem, start with browser DevTools, curl, application logs, reverse-proxy logs, and the response headers from the exact API route.

Common bad fixes to avoid

CORS issues invite shortcuts. Some make the immediate console error disappear while creating a security, reliability, or operational problem.

Do not put a private email API key in the frontend

Changing a CORS configuration so browser code can send mail directly may expose credentials that allow attackers to send spam, create cost, damage deliverability, or access account data.

Use a backend endpoint or serverless function. Your server should be the trust boundary between public user input and a privileged email-sending credential.

Do not use Access-Control-Allow-Origin: * by default

The wildcard can be appropriate for genuinely public, anonymous resources. It is not a general solution for authenticated APIs, account data, or credentialed browser sessions.

If the endpoint needs cookies or credentials: "include", return an exact trusted origin instead of *.

Do not disable browser security as a production solution

Launching a browser with web security disabled or installing a local extension may be useful for isolated debugging. It does not fix your users’ browsers, and it can hide a real configuration problem.

Do not use no-cors to “fix” an API integration

Setting:

fetch(url, { mode: "no-cors" })

does not grant your application access to a blocked API response. It produces an opaque response that JavaScript generally cannot inspect. It is not suitable for reading JSON API responses, checking status codes, or obtaining message IDs from an email API.

Do not permit every origin by reflection

Code that returns the incoming Origin value without validating it effectively authorizes any website. Maintain an explicit allowlist and review it when adding staging environments, preview deployments, custom domains, or local development ports.

Preventing CORS failures in new applications

The best CORS solution is deliberate architecture rather than reactive header changes.

Use this checklist when adding a browser-facing API:

  1. Keep privileged provider credentials server-side. Browser clients should not receive mail-sending, payment, database, or administrative API keys.
  2. Use same-origin backend routes where practical. A frontend at https://app.example.com calling https://app.example.com/api/... avoids cross-origin browser permission checks.
  3. Document permitted origins. Include production, staging, preview, and local-development URLs deliberately rather than allowing broad patterns.
  4. Handle OPTIONS before authentication redirects. A preflight needs a normal CORS approval response, not HTML for a login screen.
  5. Return CORS headers on errors as well as successes. This preserves useful error messages for the frontend.
  6. Test the deployed edge path. Verify behavior through the CDN, proxy, API gateway, and WAF—not only from a local application server.
  7. Avoid unnecessary custom headers. Every extra non-safelisted request header can require a preflight, although security and API clarity should take priority over avoiding preflights.
  8. Log an API request ID. A response header such as X-Request-ID can make it easier to correlate browser reports with gateway and application logs.

CORS configurations should be tested like any other authorization-adjacent behavior. Add automated tests for approved origins, rejected origins, preflighted methods, error responses, and credentialed requests.

Conclusion

A CORS error means a browser refused to make a cross-origin response available to your frontend JavaScript. The underlying endpoint may be healthy, but the server, proxy, or gateway did not provide the exact permissions that the browser required.

Start by identifying the page origin, inspecting the OPTIONS preflight and final response in DevTools, and reproducing the CORS exchange with curl. Then configure an explicit origin allowlist, return the right method and header permissions, include Vary: Origin for dynamic policies, and make sure error responses receive the same CORS treatment.

For transactional email applications, the more important design decision is usually architectural: send email through a backend or serverless function, not directly from browser code. That keeps API keys private, reduces abuse risk, and prevents CORS from becoming a workaround for an unsafe client-side integration.

FAQ

What does a CORS error mean in simple terms?

It means a browser blocked JavaScript on one website from reading a response from another website because the receiving server did not explicitly allow that cross-origin access.

Why does my API work in Postman but fail in the browser?

Postman and curl do not enforce browser same-origin rules. A browser checks CORS response headers before exposing a cross-origin response to JavaScript, so a missing or incorrect header can cause browser-only failures.

Can I fix CORS only in frontend JavaScript?

Usually no. The server, API gateway, proxy, or CDN serving the endpoint must return the necessary Access-Control-Allow-* response headers. Client settings cannot force an uncooperative server to allow access.

Is Access-Control-Allow-Origin: * safe?

It can be appropriate for truly public, anonymous resources. Do not use it for credentialed requests, private account data, or as a substitute for authentication and authorization.

Is a CORS error related to email DNS records?

No. CORS is an HTTP browser policy. SPF, DKIM, DMARC, MX, and other DNS records affect mail authentication, routing, and deliverability, not whether browser JavaScript can read an API response.