Skip to content

Commit 7ce0a21

Browse files
fix(client): preserve triggering error when reconnection bails at maxRetries
- Thread lastError through _scheduleReconnection so the original disconnect error is surfaced (as message suffix and Error.cause) when maxRetries is exceeded. With maxRetries=0 the previous behavior dropped the diagnostic entirely. - Align SSE-disconnected onerror message with adjacent pattern to avoid the doubled 'Error:' prefix from string-coercing an Error instance. - Add test covering the maxRetries=0 + lastError surfacing path.
1 parent e64d9ca commit 7ce0a21

2 files changed

Lines changed: 39 additions & 5 deletions

File tree

packages/client/src/client/streamableHttp.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -327,14 +327,21 @@ export class StreamableHTTPClientTransport implements Transport {
327327
*
328328
* @param lastEventId The ID of the last received event for resumability
329329
* @param attemptCount Current reconnection attempt count for this specific stream
330+
* @param lastError The error that triggered this reconnection attempt, surfaced if max retries is exceeded
330331
*/
331-
private _scheduleReconnection(options: StartSSEOptions, attemptCount = 0): void {
332+
private _scheduleReconnection(options: StartSSEOptions, attemptCount = 0, lastError?: unknown): void {
332333
// Use provided options or default options
333334
const maxRetries = this._reconnectionOptions.maxRetries;
334335

335336
// Check if we've exceeded maximum retry attempts
336337
if (attemptCount >= maxRetries) {
337-
this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));
338+
const reason = lastError === undefined ? undefined : lastError instanceof Error ? lastError.message : String(lastError);
339+
this.onerror?.(
340+
new Error(
341+
`Maximum reconnection attempts (${maxRetries}) exceeded` + (reason ? `: ${reason}` : '.'),
342+
lastError === undefined ? undefined : { cause: lastError }
343+
)
344+
);
338345
return;
339346
}
340347

@@ -347,7 +354,7 @@ export class StreamableHTTPClientTransport implements Transport {
347354
this._startOrAuthSse(options).catch(error => {
348355
this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error instanceof Error ? error.message : String(error)}`));
349356
try {
350-
this._scheduleReconnection(options, attemptCount + 1);
357+
this._scheduleReconnection(options, attemptCount + 1, error);
351358
} catch (scheduleError) {
352359
this.onerror?.(scheduleError instanceof Error ? scheduleError : new Error(String(scheduleError)));
353360
}
@@ -461,14 +468,15 @@ export class StreamableHTTPClientTransport implements Transport {
461468
onresumptiontoken,
462469
replayMessageId
463470
},
464-
0
471+
0,
472+
error
465473
);
466474
} catch (error) {
467475
this.onerror?.(new Error(`Failed to reconnect: ${error instanceof Error ? error.message : String(error)}`));
468476
}
469477
} else {
470478
// Stream disconnected and reconnection will not happen; surface the error
471-
this.onerror?.(new Error(`SSE stream disconnected: ${error}`));
479+
this.onerror?.(new Error(`SSE stream disconnected: ${error instanceof Error ? error.message : String(error)}`));
472480
}
473481
}
474482
};

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,6 +1783,32 @@ describe('StreamableHTTPClientTransport', () => {
17831783
expect(transport['_cancelReconnection']).toBeUndefined();
17841784
});
17851785

1786+
it('should surface the triggering error when maxRetries is 0', async () => {
1787+
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
1788+
reconnectionOptions: {
1789+
initialReconnectionDelay: 10,
1790+
maxRetries: 0,
1791+
maxReconnectionDelay: 1000,
1792+
reconnectionDelayGrowFactor: 1
1793+
}
1794+
});
1795+
1796+
const errorSpy = vi.fn();
1797+
transport.onerror = errorSpy;
1798+
1799+
const triggeringError = new Error('socket hang up');
1800+
transport['_scheduleReconnection']({}, 0, triggeringError);
1801+
1802+
expect(errorSpy).toHaveBeenCalledTimes(1);
1803+
expect(errorSpy).toHaveBeenCalledWith(
1804+
expect.objectContaining({
1805+
message: 'Maximum reconnection attempts (0) exceeded: socket hang up',
1806+
cause: triggeringError
1807+
})
1808+
);
1809+
expect(transport['_cancelReconnection']).toBeUndefined();
1810+
});
1811+
17861812
it('should schedule reconnection when maxRetries is greater than 0', async () => {
17871813
// ARRANGE
17881814
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {

0 commit comments

Comments
 (0)