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:

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.

Which API needs which connect-src source Six browser networking APIs on the left, each arrow pointing to the connect-src source form the browser matches the request against. Browser API Matched connect-src source fetch() / XMLHttpRequest EventSource (SSE) navigator.sendBeacon() a ping attribute WebSocket (ws) WebSocket (wss) https:// host or 'self' ws:// host wss:// host
fetch, SSE, sendBeacon and ping match http(s) sources; WebSockets need their own ws://wss:// source and are never covered by 'self'.

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.

How the browser blocks a WebSocket and logs the violation Sequence from new WebSocket() through the connect-src check to either an open socket or a console violation with no network traffic. new WebSocket(wss://…) CSP engine matches URL against connect-src list wss:// listed? yes → handshake socket opens no → blocked no packet sent Console: Refused to connect Enforcement is client-side and pre-network: a blocked socket never reaches your server logs.
A blocked WebSocket produces a console violation but zero network traffic — check the browser, not the server, when a socket "silently" fails.

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.

Wildcard scope: what each source form actually matches A matrix comparing four destination hosts against three connect-src source forms, marking each as allowed or blocked. Destination https://api.ex.com https://*.ex.com 'self' https://api.ex.com allow allow block https://cdn.ex.com block allow block https://ex.com (bare) block block block wss://api.ex.com block block block
A wildcard covers subdomains but never the bare parent host, and no https:// or 'self' source ever covers a wss:// destination.

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.