Server-Sent Events and CORS: EventSource's limits
An EventSource connection fails with something that looks like a CORS error, but the fix that works for fetch often doesn't apply: EventSource has no headers option at all, and its only cross-origin setting is withCredentials. Pointing it at a CORS proxy doesn't add a headers option either, and a proxy built to buffer responses changes what "streaming" even means once it's in the path.
EventSource isn't fetch with a different name
The constructor takes a URL and one options object, and that object has exactly one documented property: withCredentials, a boolean, default false. There's no headers field, no way to change the method, nothing like fetch's second argument. You can't attach an Authorization header, a custom X-API-Key, or anything else from JavaScript, full stop.
That absence is also why EventSource requests never preflight: a preflight exists to ask permission before a request carries something the server might not expect, and an EventSource request can't carry anything beyond what the URL and cookies already send. If the server's response is missing Access-Control-Allow-Origin, the connection fails the same way a fetch would, but you'll never see an OPTIONS request in the Network tab explaining why; there isn't one.
What withCredentials controls
Setting withCredentials: true sends cookies and TLS client certificates with the request, the same credentials mode fetch uses with credentials: 'include'. It doesn't add headers, and it doesn't let you pass an API key some other way. The server has to reciprocate: a credentialed request is rejected if Access-Control-Allow-Origin is the literal *, no matter what else the response sends, and it needs Access-Control-Allow-Credentials: true on top of a specific origin. Get that wrong and the symptom is identical to a plain missing-CORS-header failure: the connection opens, then the browser's console reports it blocked, with no event ever reaching your onmessage handler. Our piece on cookies and credentials covers the server side of that pairing in full; it applies to EventSource exactly as it does to fetch.
Pointing EventSource at corsproxy.dev
corsproxy.dev's response always carries Access-Control-Allow-Origin: * and never sends Access-Control-Allow-Credentials, so withCredentials: true against it will fail the same check described above. corsproxy.dev doesn't use cookies for authentication: your API key goes in the URL instead, which is exactly the one thing EventSource can send without a headers option.
const target = encodeURIComponent('https://api.example.com/stream');
const es = new EventSource(
`https://api.corsproxy.dev/proxy?url=${target}&key=YOUR_API_KEY`
);
es.onmessage = (e) => console.log(e.data);
es.onerror = (e) => console.error('stream error', e);
No withCredentials needed. The key in the query string is the documented way to authenticate from a browser context that can't set a header, the same reason an <img> tag or a plain anchor link would use it too.
Getting a header to the upstream when you have none to set
Say the stream itself needs an Authorization header before the upstream will open it. corsproxy.dev's reqHeaders override exists for exactly this, and because it's parsed from the query string rather than a JavaScript-set header, it works from EventSource where a real custom header wouldn't:
const params = new URLSearchParams({
url: 'https://api.example.com/stream',
key: 'YOUR_API_KEY',
reqHeaders: 'Authorization:Bearer sk-live-xxxxx'
});
const es = new EventSource(`https://api.corsproxy.dev/proxy?${params}`);
That works, and it also puts the bearer token directly in a URL that sits in the browser's address history and its own Network tab. It lands in corsproxy.dev's request log too: our 30-day request log stores the full incoming URL, and the masking we apply only strips key= and X-API-Key= values, not reqHeaders. A secret passed this way is not stored any more carefully than the URL itself. For a one-off header that isn't sensitive, reqHeaders is fine. For a real credential, store it as a managed upstream header on your API key instead: the proxy attaches it server-side by target host, the browser URL never contains it, and it never reaches the log in the first place.
Even with auth solved, it's still not a live stream
corsproxy.dev reads the entire upstream response into memory before returning anything, capped at 10 MB, the same way it handles every proxied request. We covered why in when not to use a CORS proxy: it's what lets an oversized response fail with a clean error instead of arriving cut off, but it means nothing comes back until the upstream connection closes. Point EventSource at a feed that's meant to stay open indefinitely, and onmessage simply never fires, then the request eventually fails once the response hits the 10 MB cap or the connection times out. A proxy built this way can carry authentication and CORS headers for a stream; it can't make a buffered response behave like a live one.
What to use instead
If you control the server on the other end, add Access-Control-Allow-Origin there directly (our Express CORS guide has the one-line version) and call it straight from the browser; EventSource's header and credentials limits apply whether or not a proxy sits in front. If you don't control it and it already sends CORS headers, same answer: connect directly, nothing in this post applies. A proxy only earns its place when the target has no CORS header and you can't add one, and even then, if the connection genuinely needs to stay open and stream, terminate it on a server you run instead of a stateless request/response relay, managed or self-hosted.