bigskysoftware / bigskysoftware/htmx-extensions

SSE: re-processing an element with `sse-connect` leaks the previous EventSource

Open
#190 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
284
Forks
88
PR merge metrics
No merged PRs in 30d

Description

## What happens

When htmx processes an element carrying `sse-connect` a second time, the SSE extension opens a
second `EventSource` and loses its reference to the first one. The first connection stays open
for the lifetime of the page, and nothing can ever close it, because the only handle to it is
already gone.

## Why the handle is gone

`ensureEventSource` stores the connection in the element's internal data:

```js
// src/sse/sse.js
api.getInternalData(elt).sseEventSource = source
```

But htmx wipes an element's internal data in `deInitNode()` immediately before it re-fires
`htmx:afterProcessNode`:

```js
// htmx 2.0.4, dist/htmx.js:1672, inside deInitNode()
forEach(Object.keys(internalData), function(key) { if (key !== 'firstInitCompleted') delete internalData[key] })
```

`htmx:beforeCleanupElement` is **not** fired on this path — only `cleanUpElement()` fires it,
and re-processing does not go through it. So by the time the extension's
`htmx:afterProcessNode` handler runs, `internalData.sseEventSource` is already `undefined`
while the previous `EventSource` is still `OPEN` and unreachable.

Worth flagging for anyone attempting a fix: the obvious guard

```js
if (api.getInternalData(elt).sseEventSource) { /* close it */ }
```

does **not** work. It never sees anything, for exactly the reason above. The reference has to
be kept somewhere htmx does not clear.

## Reproduction

`htmx:afterProcessNode` only re-fires when the element's attribute hash has changed, so a bare
second `htmx.process()` is not enough — any attribute change is:

```html


initial


```

```js
htmx.process(box) // connection #1
box.setAttribute('data-anything', '1') // any attribute change
htmx.process(box) // connection #2; #1 is now orphaned
```

Both connections stay open in the network panel.

The attribute change is not synthetic in practice — it is what a morph swap does. Idiomorph
preserves the node and mutates its attributes, after which htmx re-processes it. That
combination has been supported since #165.

Measured against a local SSE server that counts concurrent connections, using a real
`EventSource` (not a test mock):

```
connections open before re-process: 1
connections open after re-process: 2
connections ever opened total: 2
```

Descendants make it more confusing to diagnose: their listeners captured the *old* source
object, so the leaked connection is the one still feeding the page, while the newly created
one has no listeners at all. Everything looks like it is working.

## Impact

- Every orphan holds a connection slot on the server. In our case that is one slot out of a
per-user cap, so a user eventually locks themselves out of their own stream — and it
presents as rate limiting rather than as a leak, which cost us a while to track down.
- Browsers cap SSE at 6 connections per domain over HTTP/1.1 (already noted in #143), so a
handful of re-processes is enough to wedge the page.

## Not a duplicate of #147

#147 and htmx discussion #2109 describe a different leak: connections not closed when
navigating *away* from a page, addressed with a global `beforeunload` sweep. That does not
apply here — the element is never removed and the page never unloads.

## Versions

- htmx 2.0.4
- htmx-ext-sse 2.2.3 (also present on `main`)

---

I have a fix with tests for this and will open a PR shortly.

Contributor guide

Open the contributing guide

Research direction

Start in src/sse/sse.js, especially ensureEventSource and the htmx:afterProcessNode handler, then reproduce the issue with the attribute-change and htmx.process sequence described. Add regression coverage showing that re-processing does not leave the earlier EventSource open, and verify the connection count returns to the expected level.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.