-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Description
Describe the bug
The StreamableHTTPClientTransport fails to automatically reconnect a disconnected SSE stream if the server is not configured for resumable streams (i.e., not sending SSE id fields). The reconnection logic in the _handleSseStream error handler is incorrectly conditional on the presence of a lastEventId, causing the transport to permanently disconnect instead of attempting to establish a fresh connection.
To Reproduce
Steps to reproduce the behavior:
- Configure a server using
StreamableHTTPServerTransportwithout providing aneventStore. This ensures the server does not send SSEidfields. - Connect a client using
StreamableHTTPClientTransportand establish a long-polling SSE connection (e.g., by sending aninitializerequest). - Simulate an external network termination of the long-polling
GETrequest. This can be done by deploying the server to a cloud environment with a short request timeout (e.g., 30 seconds on Google Cloud Run) and waiting for the timeout to occur. - Observe the client's behavior. It will log the disconnect error but will not make any subsequent
fetchcalls to re-establish the connection.
Expected behavior
Upon a stream disconnect, the client should always attempt to reconnect according to its configured reconnectionOptions, regardless of whether a lastEventId is available. If no lastEventId exists, it should simply establish a new, fresh stream. The connection should be resilient to transient network failures, not brittle.
Additional context
This bug makes the client transport extremely fragile in common cloud environments (like Google Cloud Run, App Engine, etc.) that impose absolute request timeouts. These environments will terminate the long-lived SSE connection, and the client's failure to reconnect renders the application non-functional without a full page refresh.
The root cause is a faulty conditional check in the catch block of the _handleSseStream method:
// In _handleSseStream...
} catch (error) {
this.onerror?.(new Error(`SSE stream disconnected: ${error}`));
if (this._abortController && !this._abortController.signal.aborted) {
// This check incorrectly prevents reconnection for non-resumable streams
if (lastEventId !== undefined) {
this._scheduleReconnection(...);
}
}
}The if (lastEventId !== undefined) check should be removed to allow _scheduleReconnection to be called unconditionally, making the client resilient by default.