# sse.js: an EventSource replacement that accepts headers and POST bodies

> sse.js is a JavaScript Server-Sent Events client that fixes the two limitations people hit with the built-in EventSource: it can send custom request headers and it can POST a payload. It also adds configurable auto-reconnect with Last-Event-ID resume, at the cost of no longer being a browser-native stream.

**mpetazzoni/sse.js** — A flexible Server-Sent Events EventSource polyfill for Javascript

- Repository: https://github.com/mpetazzoni/sse.js
- Stars: 641 · Forks: 106
- Language: JavaScript
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/mpetazzoni-sse-js

## The two EventSource limitations the library exists to fix

The README names the problem in two sentences. sse.js is an `EventSource` replacement for JavaScript designed to consume Server-Sent Events streams with more control and options than the standard `EventSource`, and the main limitations of `EventSource` are that it only supports no-payload GET requests and does not support specifying additional custom headers to the HTTP request.

Both of those are things you discover the hard way. The first is that a stream which needs a body, such as a subscription request with a filter payload, simply cannot be expressed. The second is that an API that authenticates with a bearer token cannot be consumed at all, because the browser's EventSource gives you no way to set `Authorization` and will not let you attach a cookie cross-origin without credentials handling.

The package is designed as a drop-in. The README says it is a fully compatible `EventSource` polyfill, and shows the swap in one line:

```js
EventSource = SSE;
```

Assigning to the global is blunt but effective when you control the bundle. The `SSE` class also keeps the `EventSource` constructor shape, `var source = new SSE(url, options)`, so code written against the standard interface keeps working, and it exposes the same event listener surface.

The trade is implied by the word polyfill. A native EventSource is handled by the browser's own networking stack, with its own buffering and backpressure, and the browser can keep the connection alive across navigations in ways a JavaScript object cannot. sse.js is your JavaScript code doing the request, so you get the extra headers and the payload, and you take on the transport.

## Importing it depends on whether your code is a module

There are two documented import forms, and the non-module one is a little unusual.

From a module context the import is direct:

```js
import { SSE } from "./sse.js";
```

From a non-module context, the README shows a dynamic import wrapped in an async IIFE that assigns to `window`:

```js
(async () => {
  const { SSE } = import("./sse.js");
  window.SSE = SSE;
})();
```

The reason for the awkward shape is that the package manifest declares `"type": "module"`, so the source is an ES module and a classic script cannot use a static `import` statement. The dynamic import is what makes the module loadable from a plain script tag without a bundler, and the IIFE exists so the top-level await inside it does not leak into the global scope.

The package manifest also points at a build output rather than the TypeScript source. The `main` field is `./lib/sse.js`, the `types` field is `types/sse.d.ts`, and the build script is `tsc`, so the published artifact is compiled JavaScript with hand-maintained type declarations in a separate `types/` directory. The repository root has a `tsconfig.json`, a `.babelrc` and a `bower.json`, which is the combination of a project that started life with Babel and a Bower registry and later moved to a TypeScript build without throwing the old scaffolding away.

## The stream starts on construction unless you ask it not to

The simplest usage is three lines, and the shape is familiar from EventSource:

```js
var source = new SSE(url);
source.addEventListener("message", function (e) {
  // Assuming we receive JSON-encoded data payloads:
  var payload = JSON.parse(e.data);
  console.log(payload);
});
```

The one behavior worth reading carefully is that, like `EventSource`, `SSE` automatically executes the request and starts streaming. There is no separate call needed to begin. The README then explains how to opt out: pass the `start: false` option to be specific about when the request is triggered, and call the `stream()` method later.

```js
var source = new SSE(url, {start: false});
source.addEventListener('message', (e) => { ... });
// ... later on
source.stream();
```

That option matters more than it first appears, because `stream()` is also the reconnect primitive. Once auto-reconnect is disabled, `stream()` becomes the thing you call to retry after an error, and the same call that starts a deferred stream is the one that restarts a broken one. The README's manual reconnection example relies on exactly that, catching the `error` event, waiting, and calling `source.stream()` again, with a second variant that reconnects on the `abort` event.

The `abort` event is worth noting as a distinct concept from an error. It is the signal that the stream was torn down, and a client that reconnects on `abort` is distinguishing deliberate closure from failure.

## Custom headers and a POST payload are one option object

This is the core of the library, and the options object carries it.

Custom headers are passed with `headers`:

```js
var source = new SSE(url, { headers: { Authorization: "Bearer 0xdeadbeef" } });
```

A payload turns the request into a POST. The README says to make an HTTP POST request, simply specify a `payload` in the options:

```js
var source = new SSE(url, {
  headers: { "Content-Type": "text/plain" },
  payload: "Hello, world!",
});
```

The `method` option can override the HTTP method regardless of whether a payload is present, which is the escape hatch for the odd server that expects a GET with a body, or that wants a PUT:

```js
var source = new SSE(url, {
  headers: { "Content-Type": "text/plain" },
  payload: "Hello, world!",
  method: "GET",
});
```

Two details are easy to get wrong here. First, the `Content-Type` header is not set for you, so a server that validates content types will reject the request until you set it. Second, note that the method override example passes a payload while forcing `GET`, which is unusual but legal enough for some servers, and the README presents it as the general escape hatch rather than as a special case.

Because the headers object is applied to the initial request and to reconnect attempts, the credential travels with every retry. That is the intended behavior for a token, and it is also the reason a token in client-visible options is a consideration for a browser build, where anything in the page is readable by the page.

## Auto-reconnect with a retry cap and Last-Event-ID resume

Reconnection is the feature that moves sse.js further from EventSource, and it is configured through four options:

```js
var source = new SSE(url, {
  autoReconnect: true, // Enable auto-reconnect
  reconnectDelay: 3000, // Wait 3 seconds before reconnecting
  maxRetries: null, // Retry indefinitely (set a number to limit retries)
  useLastEventId: true, // Send Last-Event-ID header on reconnect (recommended)
});
```

The README lists the resulting behavior. The connection attempts to reconnect after any connection loss or error. Each attempt waits for the specified delay in milliseconds. If `maxRetries` is set, attempts stop after that number is reached, and `null` means retry indefinitely. The retry count resets whenever a successful connection is established, and auto-reconnect is automatically disabled when you call `close()` on the instance.

The status is readable at any time, which matters if you want to surface it in a UI:

```js
if (source.autoReconnect) {
  console.log("Auto-reconnect is enabled");
  if (source.maxRetries) {
    console.log(`Attempt ${source.retryCount} of ${source.maxRetries}`);
  }
}
```

The `Last-Event-ID` support is the part that makes reconnecting correct rather than merely automatic. The README describes the header as a central part of the SSE specification that helps maintain message continuity across reconnections, and lists the three automatic behaviors: track the last received event ID, send it in the `Last-Event-ID` header on reconnection attempts, and allow the server to resume the event stream from where it left off. The behavior is controlled by `useLastEventId` and is on by default, with the README strongly recommending you keep it enabled and only disable it if you have specific requirements that conflict.

That recommendation carries a condition worth stating plainly. The client can only send the last event ID; whether the server uses it to resume without replaying or skipping events is entirely up to the server. If the endpoint ignores `Last-Event-ID`, the header is decoration and your stream resumes with a gap or a duplicate burst.

## The event dispatch order is specified, which is unusual for a client

The README ends with a table of the order in which SSE events are dispatched, and the presence of that table at all is notable. Most clients leave you to discover the sequence empirically.

The table starts with `readystatechange` when the state changed to `CONNECTING`, fired when `stream()` is called, carrying `readyState: 0`. Then `open` when the connection is established, which happens when the server response headers are received, and it carries `responseCode` and `headers`. Then a second `readystatechange` with `readyState: 1` after the connection is established. Then `message` for data received.

Two of those properties are not part of the standard EventSource surface. `open` normally arrives as a plain event with no useful payload, while here it carries the response code and the response headers, which is exactly what you need when a stream fails with a 401 or a 302 and you need to know why. The `readystatechange` event carries a `readyState` value matching the standard constants, which lets code written against EventSource keep working.

The table in the copy available here is cut off partway through the `message` row, so the ordering of the remaining events, which would cover named server-sent events and the error case, cannot be read from it.

One more detail the README does mention is that the last event ID is available as a property at any time:

```js
console.log("Last received event ID:", source.lastEventId);
```

And the example that ties the pieces together, a client with a client identifier header, auto-reconnect and resume, that logs the event id on message and reports the resume point on open:

```js
const source = new SSE("/api/events", {
  autoReconnect: true,
  useLastEventId: true,
  headers: { "Client-ID": "dashboard-1" },
});
```

For the error case, the README's automatic reconnection example is careful about the terminal state. It checks `source.maxRetries && source.retryCount >= source.maxRetries` and logs that the connection is permanently closed, rather than reporting a failed attempt as if it might still succeed, which is the failure mode to watch for in your own logging.

## Conclusion

Adopt sse.js when your SSE endpoint needs an Authorization header, a POST body, or a resume token that the built-in EventSource cannot send, and you are willing to own the transport yourself. Do not adopt it when the standard EventSource already works for your stream, since the polyfill gives up the browser's native implementation and its connection handling. Verify first that your server honors the `Last-Event-ID` header on reconnect, since the resume behavior only helps if the other end can act on it, and read the event dispatch order table before relying on `open` or `readystatechange` to mean what you expect.

## FAQ

### Why would I use sse.js instead of the built-in EventSource?

The README names the two limitations of EventSource that sse.js removes: it only supports no-payload GET requests, and it does not support specifying additional custom headers on the HTTP request. sse.js adds a payload option, a method override and a headers option.

### How do I import sse.js in a non-module script?

Because the package is an ES module, a plain script uses a dynamic import inside an async IIFE, as the README shows: `(async () => { const { SSE } = import("./sse.js"); window.SSE = SSE; })();`. In a module context you can use `import { SSE } from "./sse.js";` directly.

### Does sse.js reconnect automatically?

Yes, when you pass `autoReconnect: true`. The README lists that it reconnects after any connection loss or error, waits `reconnectDelay` milliseconds between attempts, stops after `maxRetries` if that is set, resets the retry count on a successful connection, and disables auto-reconnect when `close()` is called.

### What does the Last-Event-ID option do in sse.js?

With `useLastEventId` enabled, which is the default, the client tracks the last received event ID, sends it in the `Last-Event-ID` header on reconnection, and lets the server resume the stream where it left off. The README recommends keeping it on since it is part of the SSE specification.

### How do I trigger the SSE request manually instead of on construction?

Pass the `start: false` option and call the `stream()` method when you want the request made. The same `stream()` call is the manual reconnect primitive when auto-reconnect is disabled, for example from an `error` or `abort` event listener.

## Sources

- [Issues](https://github.com/mpetazzoni/sse.js/issues)
- [License: Apache-2.0](https://github.com/mpetazzoni/sse.js/blob/main/LICENSE)
- [mpetazzoni/sse.js on GitHub](https://github.com/mpetazzoni/sse.js)
- [README](https://github.com/mpetazzoni/sse.js/blob/main/README.md)
- [Releases](https://github.com/mpetazzoni/sse.js/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mpetazzoni-sse-js
