CORS vs CSP connect-src Explained
Two separate browser mechanisms can stop a fetch() to another origin, and they are constantly mistaken for one another. Content-Security-Policy’s connect-src decides whether the page is even allowed to make the request; CORS decides whether the page is allowed to read the response after the server answers. They live on different servers, produce different console messages, and require different fixes.
This page is part of CORS & Related Security Header Interactions, which maps how CORS relates to the other cross-origin security layers.
The Two Error Strings
A CSP connect-src violation (Chrome):
Refused to connect to 'https://api.example.com/v1/data' because it violates the
following Content Security Policy directive: "connect-src 'self' https://cdn.example.com".
A CORS failure (Chrome):
Access to fetch at 'https://api.example.com/v1/data' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the
requested resource.
If the message contains “Refused to connect” and names a Content Security Policy directive, it is CSP — the request never left the browser. If it says “blocked by CORS policy”, the request was sent and the response was rejected.
Root Cause
The two mechanisms gate different steps of the same request. Content-Security-Policy is defined by the W3C CSP specification and is enforced on the requesting document: connect-src is an allowlist of destinations the page’s scripts may open connections to (via fetch, XMLHttpRequest, WebSocket, EventSource, sendBeacon). The browser checks it before dispatching the request. CORS, defined by the WHATWG Fetch Standard, is enforced after the server responds: it governs whether JavaScript may read a cross-origin response. Because CSP fires first, a connect-src block means the API server never even receives the request — so no amount of server-side Access-Control-Allow-Origin configuration will resolve it.
connect-src also has a fallback that catches people out: if the policy never mentions it, the browser falls back to default-src. A policy as innocuous as default-src 'self' therefore blocks every cross-origin fetch() on the page without the string connect-src appearing anywhere in the header. The directive governs fetch(), XMLHttpRequest, WebSocket, EventSource and navigator.sendBeacon(); it does not govern <img>, <script>, <link> or <iframe> loads, which are matched by img-src, script-src, style-src and frame-src instead. That distinction matters when a font or an image fails to load and the team reaches for the CORS configuration: if the console names font-src rather than connect-src, neither gate on this page is the one that fired.
Laid out end to end, the two gates sit on opposite sides of the network, each owned by a different server:
Prerequisite State
- You can see the exact console error text (open DevTools → Console).
- You can edit either the page’s
Content-Security-Policyheader (for CSP) or the API server’s CORS config.
Step-by-Step
Step 1 — Identify which gate fired
Read the console message. “Refused to connect … Content Security Policy” → CSP. “blocked by CORS policy” → CORS. This determines which server you fix. Note the second half of a CSP message as well: it echoes the directive that actually matched, so a message ending in "default-src 'self'" tells you the policy has no connect-src at all and you are looking at the fallback.
Each message fingerprint routes to exactly one owner and one place to edit:
Step 2a — Fix a CSP connect-src block
Add the API origin to the connect-src directive in the Content-Security-Policy header served by the page’s origin:
# On the server that serves the HTML page (not the API)
add_header Content-Security-Policy
"default-src 'self'; connect-src 'self' https://api.example.com" always;
Step 2b — Fix a CORS block
Add the correct Access-Control-Allow-Origin on the API server:
// On the API server that receives the fetch
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
res.setHeader('Vary', 'Origin');
Step 3 — Re-test both gates
A request can clear CSP and then hit CORS. Fix one, reload, and read the next error. Only when neither message appears is the request fully permitted. Expect the second error to appear after the first fix lands — that is progress, not a regression, and a team that reverts the CSP change because “a new error showed up” ends up chasing the same loop twice.
Treat the request as a small state machine that only advances one step per deploy:
Verification
Where the Two Gates Diverge
Once you have both gates open for the simple case, the interesting failures are the ones where only one of them applies.
Redirects. CSP re-evaluates connect-src against every URL in a redirect chain, not just the one you typed into fetch(). If https://api.example.com/v1/data answers 302 with a Location of https://cdn-api.example.net/v1/data, the second host must also be listed or the console reports a refusal naming a URL your source code never mentions. CORS treats the same chain differently: when a cross-origin redirect moves a cors-mode request to another origin, the browser tags the request with an opaque origin, so the next hop arrives carrying Origin: null and an allowlist that only knows https://app.example.com will reject it. The redirect target must therefore be present in connect-src and be prepared for a null origin — which is usually a sign that the redirect belongs on the server side instead.
WebSocket and EventSource. connect-src covers both, but CORS does not touch a WebSocket handshake at all. The upgrade request carries an Origin header that your server is expected to validate itself; there is no preflight and no Access-Control-Allow-Origin to emit. A wss:// endpoint must be listed with its own scheme, because an https://api.example.com source expression does not match wss://api.example.com. Server-Sent Events over EventSource, by contrast, are subject to both gates, because the underlying request is an ordinary GET.
Service workers. A fetch() issued from inside a service worker is governed by the CSP of the worker script’s response, not by the CSP of the page that registered it. A page-level connect-src change therefore has no effect on requests the worker makes on its own initiative, which is a common source of “the fix works in one tab and not another” reports. Set the policy on the worker script response as well.
Report-only mode. Content-Security-Policy-Report-Only never blocks anything; it only emits a securitypolicyviolation event and an optional report. If you see a violation logged for connect-src but the request still appears in the Network panel and the data arrives, you are reading a report-only policy and the actual failure lies elsewhere — almost always on the CORS side. Chrome prints report-only violations with the phrase “would have been refused”, which is the string to grep for.
Framework defaults. Helmet’s default policy for Express sets default-src 'self' with no connect-src, so every API host has to be added explicitly. Next.js and Nuxt both let you define the header in config and in middleware, and the two silently stack — the browser enforces the intersection of the two policies, so an origin allowed in one and omitted from the other is still blocked. When a policy is delivered both as a header and as a <meta http-equiv="Content-Security-Policy"> tag, the same intersection rule applies, and the <meta> form ignores frame-ancestors, report-uri and sandbox entirely.
Security Boundary Note
Do not loosen connect-src to * or drop CSP entirely just to clear the error — that removes a defense against exfiltration by injected scripts. Add only the specific API origin you need. Likewise, do not respond to a CORS error by reflecting every origin; keep the API’s allowlist tight. The two headers are independent defenses, and weakening one to fix a symptom in the other leaves both weaker.
Common Mistakes
| Issue | Technical impact | Mitigation |
|---|---|---|
| Editing API CORS headers to fix a CSP error | The request never reached the API; nothing changes | Fix connect-src on the page’s origin instead |
Adding the API to connect-src to fix a CORS error |
Request was already sent; the response is still blocked | Fix Access-Control-Allow-Origin on the API |
Setting connect-src * to make the error go away |
Removes exfiltration protection for the whole page | Allowlist only the specific API origin |
FAQ
Can a CSP connect-src violation look like a CORS error?
They produce different console messages, but developers confuse them because both stop a cross-origin fetch. A CSP violation says the request was “Refused to connect” for violating a Content-Security-Policy directive; a CORS error says the response was “blocked by CORS policy”. CSP fires first — the request is never sent — so fixing the server’s CORS headers will not help a CSP block.
Which fires first, CSP or CORS?
CSP connect-src is evaluated before the request leaves the browser. If the destination is not allowed, the browser refuses to send the request and CORS is never reached. Only if connect-src permits the destination does the request go out and CORS then govern whether the response may be read.
Do I fix connect-src on the server or the client?
connect-src is part of the Content-Security-Policy header sent by the page’s own server (or a page meta tag). You add the API origin to that directive on the document that runs the script. CORS is fixed on the API server that receives the request — a different server.
Does connect-src apply to WebSocket connections?
Yes. WebSocket, EventSource, navigator.sendBeacon and XMLHttpRequest are all governed by connect-src, and a wss:// endpoint must be listed with its own scheme because ws: and wss: do not match an https: source expression. CORS, by contrast, does not apply to a WebSocket handshake at all — the browser sends an Origin header and the server decides whether to complete the upgrade, so there is no Access-Control-Allow-Origin to configure and no preflight.
Why is CSP still blocking a URL that is already in my connect-src list?
The three usual causes are a source expression that carries a path, a redirect, and a scheme mismatch. A source with a path segment matches by path prefix, so https://api.example.com/v1 will not match /v2. A redirect to a host that is not itself listed is refused even though the first hop was allowed. And an https: source never matches a ws:, wss: or blob: URL. Check the directive the console names — if it says default-src, your policy has no connect-src at all and is falling back, as described in how browsers evaluate the same-origin policy.