Skip to content

Commit 40cc90d

Browse files
committed
fix: allow streamable HTTP restart after close
1 parent cc4b416 commit 40cc90d

3 files changed

Lines changed: 24 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@modelcontextprotocol/client': patch
3+
---
4+
5+
Allow StreamableHTTPClientTransport to be restarted after close by clearing its aborted controller.

packages/client/src/client/streamableHttp.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,8 @@ export class StreamableHTTPClientTransport implements Transport {
524524
// `StartSSEOptions`) must honour the per-request abort exactly as the
525525
// original POST did — both as a fetch signal and as a "do not surface
526526
// onerror" gate.
527-
const isIntentionalAbort = (): boolean => this._abortController?.signal.aborted === true || requestSignal?.aborted === true;
527+
const transportSignal = this._abortController?.signal;
528+
const isIntentionalAbort = (): boolean => transportSignal?.aborted === true || requestSignal?.aborted === true;
528529

529530
try {
530531
// Try to open an initial SSE stream with GET to listen for server messages
@@ -683,9 +684,9 @@ export class StreamableHTTPClientTransport implements Transport {
683684
// Honour BOTH the transport-wide abort and the per-request abort
684685
// (a listen subscription closed during the backoff delay): do not
685686
// resurrect a stream the caller already tore down.
686-
if (this._abortController?.signal.aborted || options.requestSignal?.aborted) return;
687+
if (!this._abortController || this._abortController.signal.aborted || options.requestSignal?.aborted) return;
687688
this._startOrAuthSse(options).catch(error => {
688-
if (this._abortController?.signal.aborted || options.requestSignal?.aborted) return;
689+
if (!this._abortController || this._abortController.signal.aborted || options.requestSignal?.aborted) return;
689690
this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error instanceof Error ? error.message : String(error)}`));
690691
try {
691692
this._scheduleReconnection(options, attemptCount + 1);
@@ -719,7 +720,8 @@ export class StreamableHTTPClientTransport implements Transport {
719720
// a clean shutdown: no misleading "SSE stream disconnected" onerror,
720721
// and no GET+Last-Event-ID reconnect that would resurrect a stream the
721722
// caller just tore down.
722-
const isIntentionalAbort = (): boolean => this._abortController?.signal.aborted === true || requestSignal?.aborted === true;
723+
const transportSignal = this._abortController?.signal;
724+
const isIntentionalAbort = (): boolean => transportSignal?.aborted === true || requestSignal?.aborted === true;
723725

724726
let lastEventId: string | undefined;
725727
// Track whether we've received a priming event (event with ID)
@@ -914,6 +916,7 @@ export class StreamableHTTPClientTransport implements Transport {
914916
} finally {
915917
this._cancelReconnection = undefined;
916918
this._abortController?.abort();
919+
this._abortController = undefined;
917920
this.onclose?.();
918921
}
919922
}

packages/client/test/client/streamableHttp.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,18 @@ describe('StreamableHTTPClientTransport', () => {
267267
await reconnectTransport.close().catch(() => {});
268268
});
269269

270+
it('can be started again after close()', async () => {
271+
await transport.start();
272+
const firstAbortController = transport['_abortController'];
273+
274+
await transport.close();
275+
276+
expect(firstAbortController?.signal.aborted).toBe(true);
277+
expect(transport['_abortController']).toBeUndefined();
278+
await expect(transport.start()).resolves.toBeUndefined();
279+
expect(transport['_abortController']).toBeDefined();
280+
});
281+
270282
it('should terminate session with DELETE request', async () => {
271283
// First, simulate getting a session ID
272284
const message: JSONRPCMessage = {

0 commit comments

Comments
 (0)