Security

CSP vs CORS: why your fetch is blocked

A fetch() call dies in the console with something about a security policy, and it's not obvious whether the browser is enforcing CORS or Content-Security-Policy — the wording looks similar and both end with your request never leaving, or its response never reaching your code. They're unrelated checks with unrelated fixes. Here's how to tell them apart and fix the one you actually have.

· ~6 min read

Two different errors, one console tab

Both errors show up in DevTools as a red line the moment a fetch() or XMLHttpRequest fails cross-origin, and both mention a "policy". That's where the similarity ends. One is the server's response getting hidden from your JavaScript after the request already happened. The other is the browser refusing to let the request happen at all, based on rules the page itself shipped.

What CORS is actually checking

CORS (Cross-Origin Resource Sharing) is a check the browser runs against headers the target server sends back. If Access-Control-Allow-Origin doesn't include your page's origin, the browser throws the response away. The request itself usually still went out — you'll often see a 200 in the Network tab even though your code never sees the body. We cover the full mechanics in Why am I getting a CORS error?, so this post won't repeat it.

The key point for this post: CORS is entirely a property of the server you're calling. You can't fix a CORS error by changing anything on your own page.

What CSP is actually checking

Content-Security-Policy is a check the browser runs against rules your own page sent, via a Content-Security-Policy response header (or a <meta http-equiv="Content-Security-Policy"> tag). It's an allowlist for what the page is permitted to load, connect to, execute, or embed — scripts, styles, images, frames, and network connections all have their own directive.

The directive that blocks fetch(), XMLHttpRequest, WebSocket, and EventSource calls is connect-src. If a page ships:

Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.corsproxy.dev

then any fetch() to a host not in that list is refused before a single byte goes over the wire — regardless of whether that host would have returned perfectly good CORS headers. connect-src is a separate, independent gate from CORS: it doesn't replace CORS, and good CORS headers don't get you past it. Passing a CORS check and passing a CSP check are two unrelated conditions your request has to clear.

Telling them apart from the message text

The two console messages are distinct enough once you know what to look for:

CORS error — mentions the response and Access-Control-Allow-Origin:

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

CSP violation — mentions "Content Security Policy directive" and names the directive (usually connect-src or the default-src it fell back to):

Refused to connect to 'https://api.example.com/users' because it
violates the following Content Security Policy directive:
"connect-src 'self'".

If you see "Content Security Policy directive" anywhere in the message, changing CORS settings on the target server will do nothing — the request never reached it.

  CORS CSP (connect-src)
Who sets the ruleThe target serverYour own page
Where it's checkedOn the responseBefore the request is sent
Fixed byServer config, or a proxyYour own CSP header
Console mentionsAccess-Control-Allow-Origin"Content Security Policy directive"

Fixing a CSP block

Add the host you're calling to connect-src in whatever sets your CSP header. In Express, for example:

app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; connect-src 'self' https://api.corsproxy.dev https://api.example.com"
  );
  next();
});

A <meta http-equiv="Content-Security-Policy"> tag works too, but a real response header is preferable — the meta tag can't set every directive (notably frame-ancestors and report-uri), and it only applies from the point it appears in the document, not to requests made before the parser reaches it.

Don't fix this by deleting connect-src or widening it to * — that's the directive doing its job, which is limiting exfiltration if something on the page is later compromised. List the specific hosts you actually call.

The gotcha when you add a CORS proxy

If you're already routing a call through corsproxy.dev to work around a missing Access-Control-Allow-Origin header, and the page also has a CSP with a locked-down connect-src, you need to allowlist the proxy's origin — not the upstream API's. Your fetch() only ever connects to api.corsproxy.dev; the proxy is the one making the second hop to the upstream server-side, where CSP doesn't apply at all:

// Page CSP needs: connect-src 'self' https://api.corsproxy.dev
// (api.example.com does NOT need to be in connect-src — the browser
// never talks to it directly)
fetch('https://api.corsproxy.dev/proxy?url=' + encodeURIComponent('https://api.example.com/users'), {
  headers: { 'X-API-Key': 'YOUR_CORSPROXY_KEY' }
})
  .then(r => r.json())
  .then(data => console.log(data));

Adding api.example.com to connect-src in that setup is harmless but pointless — the browser has no direct connection to it to permit.

Try it

Get a free API key — 500 requests/day, no credit card. Add api.corsproxy.dev to your connect-src once, not one entry per upstream you call through it.