---
title: "SSE / EventSource Does Not Support Custom Headers — Pass JWT as Query Param"
aliases: [sse-no-headers, eventsource-jwt, sse-auth-token]
tags: [sse, eventsource, jwt, auth, browser, security, gotcha]
sources:
  - "daily/2026-05-06.md"
created: 2026-05-06
updated: 2026-05-06
---

# SSE / EventSource Does Not Support Custom Headers — Pass JWT as Query Param

The browser-native `EventSource` API cannot set custom HTTP headers. There is no `headers` option in the constructor. For JWT-authenticated SSE streams, the token must be appended as a query parameter (`?token=...`). This is a known browser limitation with no workaround short of switching to `fetch()` with a `ReadableStream`.

## Key Takeaways

- `new EventSource(url)` sends only cookies and browser-default headers — no way to add `Authorization: Bearer`
- JWT must travel as a URL query parameter: `?token=<jwt>` for browser-native SSE
- Query-param tokens are visible in server access logs — use short-lived tokens (< 60 s) and rotate them
- Alternative: use `fetch()` + `ReadableStream` (SSE-over-fetch) which does support custom headers, but requires manual reconnect logic
- Backend must accept both `Authorization` header (for regular API calls) and `?token=` (for SSE)

## Details

### The Problem

```javascript
// ❌ This is the intuitive approach — but EventSource has no headers option
const es = new EventSource("/stream", {
    headers: { Authorization: `Bearer ${token}` }  // IGNORED — not a valid option
});
```

### Fix: Token as Query Parameter

```javascript
// ✅ Pass token as query param
const token = localStorage.getItem("access_token");
const es = new EventSource(`/stream?token=${encodeURIComponent(token)}`);

es.onmessage = (event) => {
    const data = JSON.parse(event.data);
    console.log(data);
};

es.onerror = (err) => {
    es.close();
    // Reconnect logic here
};
```

### FastAPI Backend: Accept Both Header and Query Param

```python
from fastapi import Query, Header, HTTPException
from typing import Optional

async def get_current_user_sse(
    token: Optional[str] = Query(None),
    authorization: Optional[str] = Header(None),
):
    """Auth handler that accepts JWT from header OR query param (for SSE)."""
    raw_token = None
    if authorization and authorization.startswith("Bearer "):
        raw_token = authorization.removeprefix("Bearer ")
    elif token:
        raw_token = token
    else:
        raise HTTPException(status_code=401, detail="Missing token")
    
    return verify_jwt(raw_token)  # your existing JWT verifier

@app.get("/stream")
async def stream_events(
    request: Request,
    user = Depends(get_current_user_sse),
):
    async def event_generator():
        while True:
            if await request.is_disconnected():
                break
            yield {"data": json.dumps({"status": "ok"})}
            await asyncio.sleep(2)
    
    return EventSourceResponse(event_generator())
```

### Alternative: fetch() + ReadableStream (Supports Headers)

```javascript
// ✅ Full header support, but you must implement reconnect manually
const response = await fetch("/stream", {
    headers: { Authorization: `Bearer ${token}` },
    signal: abortController.signal,
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const text = decoder.decode(value);
    // Parse SSE text format manually: "data: {...}\n\n"
    processSSEChunk(text);
}
```

### Security Considerations

| Approach | Token exposure | Reconnect | Browser support |
|----------|---------------|-----------|-----------------|
| `?token=` query param | Server logs, referrer headers | Automatic | Universal |
| `fetch()` + ReadableStream | Headers only (safe) | Manual | Modern browsers |
| Cookie (httpOnly) | Minimal | Automatic | Universal, requires CORS cookie config |

> [!tip] Prefer short-lived tokens for SSE
> If using query param auth, generate a dedicated short-lived SSE token (60–300 s) via a separate endpoint. This limits the window of exposure if a token is leaked via logs.

## Related

- [[wiki/concepts/websocket-react-token-guard]] — WebSocket token guard pattern (similar auth-before-connect problem)
- [[wiki/concepts/double-submit-cookie-csrf]] — alternative cookie-based stateless auth that avoids the query-param exposure
- [[wiki/concepts/zustand-async-hydration]] — Zustand hydration delay that can cause token=null on first SSE connect

## Sources

- [[daily/2026-05-06.md]] — BAIC dashboard live feed: EventSource rejecting Authorization header
