Fix SSE streaming through the internal cross-node proxy - #623
Merged
digitaldan merged 1 commit intoJul 4, 2026
Merged
Conversation
Proxied requests that land on a cloud node not holding the openHAB Socket.IO connection are re-proxied internally to the correct node via proxyToServer(). That internal http.request used a 5s `timeout`, which Node applies as a socket idle timeout for the life of the connection — so long-lived streaming responses (the REST SSE endpoints /rest/events and /rest/events/states) were torn down after the first quiet gap between events, collapsing the whole stream. - Clear the idle timeout once the upstream response begins streaming, so SSE responses that sit idle between events are not destroyed. The timeout now only guards connection setup / time-to-first-byte, and is bumped 5s -> 10s to be less aggressive on a slow target node. - Destroy the internal proxy request when the client disconnects (res 'close'), since .pipe() does not propagate the client-side close to the source request. This reaps the upstream connection immediately and removes any reliance on an idle timeout for teardown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Dan Cunningham <dan@digitaldan.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Server-Sent Events (SSE) proxied through the cloud — e.g.
GET /rest/events/statesand/rest/events— close after the first event instead of staying open.The cloud runs multiple node processes behind nginx, which routes proxied requests to the node holding the openHAB Socket.IO connection via the
CloudServeraffinity cookie. A client without that cookie (any first request, a REST/CLI client, etc.) is round-robined to an arbitrary node. When that node isn't the one holding the connection,ensureServerre-proxies the request internally to the correct node viaproxyToServer().That internal
http.requestwas created withtimeout: 5000. Node applies thetimeoutoption as a socket idle timeout for the life of the connection, not just connection setup. So a long-lived streaming response that goes quiet between events (e.g. an unsubscribed states stream between heartbeats) tripped the 5s idle timeout, the internal request was destroyed, and the whole stream collapsed — the client saw the connection drop shortly after the first event.Fix
All in
proxyToServer(src/routes/middleware.ts):proxyReq.setTimeout(0)in the response callback). The timeout now only guards connection setup / time-to-first-byte, so a dead or unreachable target node still fails fast, but a healthy streaming response is no longer torn down between events.res.on('close'))..pipe()does not propagate the client-side close to the source request, so with the idle timeout gone this is what reaps the upstream connection when the client goes away — making teardown deterministic and immediate rather than relying on any idle timeout.Testing
npm run typecheck— cleanensureServerunit tests pass (two mockresfixtures updated to include anonstub, since the real ExpressResponseis an EventEmitter).GET /rest/events/statesproxied through a node that internally re-proxies to the connection-holding node stays open across heartbeat gaps; without it the stream dropped ~5s after the last event.