CSP connect-src for APIs & WebSockets
This guide is part of the Content-Security-Policy (CSP) reference and covers the one directive that governs every network request your JavaScript initiates. connect-src is the CSP directive that controls the destinations a page may open programmatic connections to. It applies to fetch(), XMLHttpRequest, WebSocket, EventSource (Server-Sent Events), navigator.sendBeacon(), and the ping attribute on anchors. When a call targets an origin not listed in connect-src, the browser blocks it before a single byte leaves the machine and emits a console violation. Get this directive wrong and your API calls, live sockets, and analytics beacons all fail silently from the user’s point of view.
Configuration Syntax & Exact Values
connect-src takes a space-separated list of source expressions. A request is allowed only if its destination URL matches at least one source in the list. If connect-src is absent, it falls back to default-src; if both are absent, connections are unrestricted.
Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.example.com https://*.metrics.example.com wss://live.example.com
Annotated breakdown of each source in that connect-src value:
'self'— the page’s own origin (scheme + host + port). Covers same-originfetch/XHR. It does not cover WebSocket connections to your own host: awss://upgrade is a distinct scheme match and must be listed separately (see below).https://api.example.com— an exact HTTPS host. Matcheshttps://api.example.com/anythingon the default port 443. It does not match subdomains and does not matchwss://.https://*.metrics.example.com— a wildcard host. The*matches exactly one or more leading DNS labels, so it allowshttps://eu.metrics.example.comandhttps://a.b.metrics.example.combut not the barehttps://metrics.example.com. To allow both, listhttps://metrics.example.com https://*.metrics.example.com.wss://live.example.com— a WebSocket Secure endpoint. WebSocket URLs use thews:/wss:schemes, which are matched literally. Awss://connection is not covered by'self'or by anhttps://source with the same host — you must list thewss://origin explicitly.
The scheme rule is the single most common source of blocked WebSockets. Browsers treat ws:/wss: as their own scheme sources. Listing https://live.example.com does not authorise wss://live.example.com, and 'self' never authorises a socket. The figure below maps each browser API to the destination form connect-src must contain.
Server-Side Configuration
Send connect-src as part of the single Content-Security-Policy response header. Do not split CSP across multiple headers per directive — browsers intersect multiple CSP headers, which silently tightens your policy. Configuration for Nginx, Apache, Cloudflare, Django/FastAPI and Helmet/Express follows.
Nginx
add_header Content-Security-Policy "default-src 'self'; connect-src 'self' https://api.example.com wss://live.example.com; report-uri /csp-report" always;
The always flag is mandatory: without it add_header skips responses whose status is not 2xx/3xx, so your CSP would vanish on the exact 4xx/5xx error pages where an attacker probes hardest. Note that a single add_header in a nested location replaces every inherited add_header from the server/http scope, so repeat the full CSP wherever you add other headers.
Apache
Header always set Content-Security-Policy "default-src 'self'; connect-src 'self' https://api.example.com wss://live.example.com; report-uri /csp-report"
Header always set writes the header onto the internal error-response table too, guaranteeing the policy survives on 4xx/5xx pages. Plain Header set (without always) only touches successful responses.
Cloudflare (Workers / Transform Rules)
// Cloudflare Worker: attach connect-src to every response
export default {
async fetch(request, env, ctx) {
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.set(
"Content-Security-Policy",
"default-src 'self'; connect-src 'self' https://api.example.com wss://live.example.com"
);
return new Response(response.body, { status: response.status, headers });
},
};
Cloudflare’s edge does not add connect-src for you. Setting the header on a cloned Headers object and reusing response.status guarantees the policy ships on error responses, mirroring the always behaviour on origin servers.
Django / FastAPI
# Django middleware — one CSP header on every response
class CSPMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response["Content-Security-Policy"] = (
"default-src 'self'; "
"connect-src 'self' https://api.example.com wss://live.example.com"
)
return response
Assigning response["Content-Security-Policy"] in middleware runs for every view including error responses, so the header is present on 404/500 pages. Do not gate the assignment on response.status_code.
Express / Helmet
const helmet = require("helmet");
app.use(
helmet.contentSecurityPolicy({
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
connectSrc: ["'self'", "https://api.example.com", "wss://live.example.com"],
},
})
);
Helmet serialises connectSrc into the single Content-Security-Policy header on every response the middleware wraps, including errors passed through the stack. Set useDefaults: false so Helmet’s default connect-src 'self' does not silently drop your API and socket origins.
Diagnostic & Verification Steps
First confirm the header is present and the value is exactly what you shipped:
curl -sI https://www.example.com/ | grep -i content-security-policy
Expected output (a single header line containing your connect-src):
content-security-policy: default-src 'self'; connect-src 'self' https://api.example.com wss://live.example.com
If two content-security-policy lines appear, a proxy or framework added a second policy and the browser will enforce the intersection of both — the most common cause of a call that is listed yet still blocked.
Next, reproduce a block in the browser and read the violation. Open DevTools → Console and trigger the call. A blocked fetch prints:
Refused to connect to 'https://api.other.com/v1/data' because it violates
the following Content Security Policy directive: "connect-src 'self'
https://api.example.com wss://live.example.com".
Read that message field by field: the quoted URL after “Refused to connect to” is the destination the browser tried and blocked; the directive after “violates … directive:” is the effective connect-src it checked against. If the blocked URL is wss://… but your directive only lists https://…, the fix is a wss:// source, not a wider https:// source. The sequence below traces one blocked WebSocket from the socket handshake to the console line.
Finally, verify the socket path end to end without a browser. A raw handshake against your endpoint confirms the server side is healthy so any failure is purely CSP:
curl -sv --http1.1 \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://live.example.com/socket 2>&1 | grep -i "101\|upgrade"
Expected output on a working endpoint:
< HTTP/1.1 101 Switching Protocols
< Upgrade: websocket
A 101 Switching Protocols here proves the endpoint upgrades correctly; if the browser still blocks it, the fault is a missing wss:// source in connect-src, not the server.
Edge Cases, Security Implications & Safe Rollback
Three traps account for nearly every connect-src incident.
Trap 1 — the wss:// scheme gap. As above, 'self' and https://host never authorise wss://host. On a page served over HTTPS, browsers also block plaintext ws:// as mixed content regardless of CSP, so production sockets must be wss://. List the exact wss:// origin, including a non-default port if you use one (wss://live.example.com:8443).
Trap 2 — third-party SDKs. Analytics, error trackers, chat widgets and payment scripts each open their own fetch/beacon/socket connections. A tight connect-src 'self' breaks all of them at once, and the failures surface only in the console. Enumerate every SDK’s documented endpoints before tightening. The comparison below shows how one wildcard decision changes what is allowed.
Trap 3 — redirects. connect-src is checked against the request URL and re-checked after each redirect. A fetch to an allowed origin that 302-redirects to an unlisted origin is blocked at the redirect. The violation names the pre-redirect URL, which makes it look like an allowed call was blocked. When a listed origin reports a block, inspect the Network panel for a redirect hop to an unlisted host.
Security implication. connect-src is a data-exfiltration control. Under an XSS foothold, a permissive connect-src * or connect-src https: lets injected script POST stolen data anywhere. Keep the list to an explicit allowlist of the exact API and socket origins your app needs; never use *. Pair it with HSTS so those origins are always reached over TLS.
Safe rollback. Ship the new connect-src in report-only mode first so nothing is enforced while you collect violations:
Content-Security-Policy-Report-Only: default-src 'self'; connect-src 'self' https://api.example.com wss://live.example.com; report-uri /csp-report
Content-Security-Policy-Report-Only logs every would-be block without breaking a single request. Watch the reports for a full traffic cycle, add any legitimate origin the reports reveal, then swap the header name to the enforcing Content-Security-Policy. To roll back an over-tight enforced policy instantly, flip the same value back to -Report-Only — one header-name change, no code deploy.
Conclusion
Land connect-src in stages. Validate the exact source list on staging with curl and the DevTools console, ship it enforced there behind a short max-age alongside any other CSP directives, then run it in Content-Security-Policy-Report-Only on production for one full traffic cycle to catch third-party endpoints. Only after the report stream is clean do you promote the identical value to the enforcing Content-Security-Policy header for the full production rollout.
Frequently Asked Questions
Does connect-src control <img>, <script> or <link> loads?
No. connect-src only governs programmatic connections — fetch, XHR, WebSocket, EventSource, sendBeacon, and ping. Resource loads for images, scripts, styles and fonts are governed by img-src, script-src, style-src and font-src respectively. A blocked <script src> is never a connect-src problem.
Why is my WebSocket blocked when the same host works for fetch?
Because ws:/wss: are distinct schemes. A connect-src entry of 'self' or https://live.example.com authorises fetch to that host but not a wss://live.example.com socket. Add the explicit wss://live.example.com source and the socket opens.
Does connect-src 'self' allow a WebSocket back to my own origin?
No. 'self' matches the page’s scheme, host and port for http(s) requests, but a wss:// upgrade is a separate scheme match that 'self' does not cover. You must list your own socket origin, e.g. connect-src 'self' wss://www.example.com.
Will connect-src * allow everything safely?
connect-src * matches any http/https destination but, per spec, the * wildcard does not cover ws:/wss:, blob: or data: schemes — sockets still break. More importantly * removes the exfiltration control entirely, letting injected script phone home anywhere. Use an explicit origin allowlist instead.
Where do blocked connections show up — server logs or the browser?
Only the browser. CSP enforcement is client-side and happens before any packet leaves the machine, so a blocked fetch or socket never reaches your server or its logs. Read the DevTools console violation, or collect report-uri/report-to reports, to see what was blocked.