From 60da0b87ff9ba8aa0fa199359e15b94cd046a882 Mon Sep 17 00:00:00 2001 From: Carson Gross Date: Thu, 27 Aug 2026 19:35:03 -0600 Subject: [PATCH] release sse request when stream setup throws --- src/ext/hx-sse.js | 4 ++++ test/tests/ext/hx-sse.js | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/ext/hx-sse.js b/src/ext/hx-sse.js index 789a82c89..7f7106372 100644 --- a/src/ext/hx-sse.js +++ b/src/ext/hx-sse.js @@ -398,6 +398,10 @@ let releaseRequest; ctx.extensionPromise = new Promise(resolve => releaseRequest = resolve); handleSSEResponse(ctx, releaseRequest).catch(e => { + // a throw before the stream loop skips the finally, so release + // the request here and tear the connection down + releaseRequest(); + cleanup(element); // an aborted stream is a normal end, not an error if (e.name !== 'AbortError') { api.triggerHtmxEvent(element, 'htmx:sse:error', {error: e, url: ctx.request.action}); diff --git a/test/tests/ext/hx-sse.js b/test/tests/ext/hx-sse.js index 0e8be82a6..3b6d31f63 100644 --- a/test/tests/ext/hx-sse.js +++ b/test/tests/ext/hx-sse.js @@ -62,6 +62,30 @@ describe('hx-sse SSE extension', function() { stream.close(); }); + it('releases the request when setup throws', async function() { + mockStreamResponse('/setup-throw'); + createProcessedHTML('
Loading
'); + + // a throw before the stream loop skips the finally that normally releases + let original = Object.getOwnPropertyDescriptor(htmx.config, 'sse'); + Object.defineProperty(htmx.config, 'sse', { + configurable: true, + get() { throw new Error('config blew up') } + }); + + try { + find('button').click(); + await htmx.timeout(50); + assert.isFalse(find('#spin').classList.contains('htmx-request'), + 'Indicator must hide when setup throws'); + assert.isUndefined(find('button')._htmx?.sse, + 'Connection must not stay registered when setup throws'); + } finally { + delete htmx.config.sse; + if (original) Object.defineProperty(htmx.config, 'sse', original); + } + }); + it('continuous stream reconnects with exponential backoff', async function() { const stream = mockStreamResponse('/reconnect'); createProcessedHTML('');