skip to content

You need to open an EventSource in the browser against an API on another origin and have the request authenticated. What can and cannot the EventSource constructor do about request headers and credentials, and what has to be true on the server?

level: middleimportance: should knowfreq 45%

answer

  1. only one option in the constructor
  2. no headers, no method, no body
  3. withCredentials is the whole toggle
  4. wildcard origin fails with credentials
  5. the retry re-sends the original URL

basics

~20 s

EventSource accepts only a URL and { withCredentials }. It cannot set an Authorization header, cannot change the method from GET, and sends no body. Cross-origin credentialed streams need the server to allow that exact origin and credentials.

solid answer

~40 s

The constructor is deliberately tiny: `new EventSource(url, { withCredentials: true })`. There is no header option, so you cannot attach `Authorization`, no method option — it is always GET — and no body. That leaves three practical auth routes. **Cookies**: set `withCredentials: true` so the browser attaches cookies cross-origin, which requires the server to answer with your exact origin in `Access-Control-Allow-Origin` plus `Access-Control-Allow-Credentials: true`; a wildcard origin is rejected for credentialed requests. **A token in the query string**, which works everywhere but lands the token in server logs, proxy logs and browser history. **Abandon `EventSource`** and read the stream yourself with `fetch`, which is the usual reason teams do. Remember every automatic reconnect re-sends the original URL, so a token embedded in it expires while cookies keep working.

code

javascript · 13 lines
javascript
const es = new EventSource('https://api.example.com/stream', {
  withCredentials: true,
});

es.addEventListener('message', (event) => {
  console.log(event.origin, event.data);
});

es.addEventListener('error', () => {
  if (es.readyState === EventSource.CLOSED) {
    console.log('rejected — check the credentialed CORS response headers');
  }
});

go deeper

for a junior

Know that EventSource takes only a URL and { withCredentials }, that it is always a GET, and that you cannot attach an Authorization header to it.

for a middle

Explain the consequences of that limited surface: cookies with withCredentials plus a matching credentialed CORS response, a query-string token, or reading the stream with a configurable request instead.

for a senior

Weigh the options out loud — token leakage into logs and history, the reconnect that re-sends a stale URL token, and what you give up in built-in reconnection when you move off EventSource.

for a principal

Own the policy: whether stream endpoints may accept credentials in a URL at all, how short-lived those tokens must be, and whether the platform standardises on cookie-authenticated streams so every client behaves the same.

## The constructor is the whole API surface `EventSource` takes a URL and one optional dictionary whose only meaningful member is `withCredentials`. That is it. Compared with `fetch`, which accepts a rich init object, `EventSource` gives you no control over: - **headers** — you cannot send `Authorization`, an API key header, a tenant header, or a custom trace header; - **method** — it is always GET; - **body** — there is none, so subscription parameters have to go in the query string; - **cancellation via a signal** — you stop it with `es.close()`, not an `AbortSignal`. Every authentication design for a browser `EventSource` is a consequence of that list. ## Route 1: cookies Same-origin, cookies are attached automatically and there is nothing to configure. Cross-origin, they are not — unless you opt in: ```js const es = new EventSource('https://api.example.com/stream', { withCredentials: true, }); ``` The flag only states the client's intent; the server still has to agree. For a credentialed cross-origin request the browser requires the response to name your exact origin in `Access-Control-Allow-Origin` and to include `Access-Control-Allow-Credentials: true`. A wildcard `*` is not accepted for credentialed requests, and if either header is missing the browser refuses the response — which, because it is not a valid stream response, ends the `EventSource` permanently rather than retrying. Cross-site cookies additionally need attributes that permit cross-site sending, which is a cookie-policy decision your platform team usually already owns. Cookies are the option that survives reconnection best: the browser re-attaches whatever is currently valid on every retry, so a session refreshed in another tab is picked up automatically. ## Route 2: a token in the URL ```js const es = new EventSource(`/stream?access_token=${token}`); ``` This works in every browser and needs nothing from CORS when same-origin, which is why it is common. The costs are real: URLs are written to access logs on every hop, kept in proxy logs, stored in browser history, and are visible to anything that can read the request line. If you do this, use a short-lived token minted for this purpose only, scoped to the stream, and never the same long-lived token your API uses elsewhere. The reconnect behaviour is the trap. The browser retries **the URL you gave it**, forever, unchanged. When the embedded token expires, every retry now carries a dead credential; the server answers 401, the browser sees a non-200 response and closes the stream for good. So a URL-token design must pair with an `error` handler that detects the permanent close, mints a fresh token, and constructs a new `EventSource`. ## Route 3: stop using EventSource Because the header restriction is absolute, teams whose auth is header-based often keep the server side unchanged and read the stream on the client with a request they *can* configure — which supports any header, any method and cancellation. The trade is explicit: you give up the browser's built-in reconnection and the automatic resume, and now own that logic yourself. That is a fair trade for a team that already has a request layer with auth, retry and tracing built in; it is a poor trade for a small app that wanted `EventSource` precisely because reconnection was free. ## Practical checks A credentialed stream that never opens is almost always one of four things: `withCredentials` not set, the server echoing `*` instead of the concrete origin, `Access-Control-Allow-Credentials` missing, or a cookie that is not eligible to be sent cross-site. Read the failing response in DevTools directly — because a rejected response is a permanent failure, you will typically see exactly one attempt and one `error` event rather than a retry loop, which is itself a useful diagnostic signal. One more subtlety worth knowing: `withCredentials` is exposed as a read-only property on the instance, so you cannot flip it after construction. Changing the auth mode means building a new `EventSource`.

  • Why does a token embedded in the EventSource URL eventually kill the stream, when cookies do not?
    Because the browser retries the exact URL you constructed it with and never rewrites it. Once the embedded token expires, every automatic reconnect presents a dead credential, the server answers 401, and a non-200 response ends the stream permanently. Cookies are re-read from the jar on each retry, so a refreshed session is picked up transparently. With a URL token you must detect the permanent close and build a new EventSource with a fresh one.
  • Does an EventSource request trigger a CORS preflight?
    No. It is a plain GET with no author-supplied headers, which is precisely the shape that does not need one — that is a side effect of the constructor's limits, not a special exemption. What the server must still return, when `withCredentials` is set, is the concrete requesting origin plus `Access-Control-Allow-Credentials: true`; a wildcard origin is rejected for credentialed requests.

saying these in an interview costs you the question

  • Passing a headers object to the EventSource constructor
  • Thinking withCredentials alone makes cross-origin cookies work
  • Expecting Access-Control-Allow-Origin: * to allow credentials
  • Assuming the retry picks up a refreshed token in the URL
  • Believing EventSource can POST a subscription body

context