Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/client/streamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,68 @@ describe('StreamableHTTPClientTransport', () => {
});
});

describe('Reconnection Logic with maxRetries 0', () => {
let transport: StreamableHTTPClientTransport;

// Use fake timers to control setTimeout and make the test instant.
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());

it('should not schedule any reconnection attempts when maxRetries is 0', async () => {
// ARRANGE
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
reconnectionOptions: {
initialReconnectionDelay: 10,
maxRetries: 0, // This should disable retries completely
maxReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1
}
});

const errorSpy = vi.fn();
transport.onerror = errorSpy;

// ACT - directly call _scheduleReconnection which is the code path the fix affects
transport['_scheduleReconnection']({});

// ASSERT - should immediately report max retries exceeded, not schedule a retry
expect(errorSpy).toHaveBeenCalledTimes(1);
expect(errorSpy).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Maximum reconnection attempts (0) exceeded.'
})
);

// Verify no timeout was scheduled (no reconnection attempt)
expect(transport['_reconnectionTimeout']).toBeUndefined();
});

it('should schedule reconnection when maxRetries is greater than 0', async () => {
// ARRANGE
transport = new StreamableHTTPClientTransport(new URL('http://localhost:1234/mcp'), {
reconnectionOptions: {
initialReconnectionDelay: 10,
maxRetries: 1, // Allow 1 retry
maxReconnectionDelay: 1000,
reconnectionDelayGrowFactor: 1
}
});

const errorSpy = vi.fn();
transport.onerror = errorSpy;

// ACT - call _scheduleReconnection with attemptCount 0
transport['_scheduleReconnection']({});

// ASSERT - should schedule a reconnection, not report error yet
expect(errorSpy).not.toHaveBeenCalled();
expect(transport['_reconnectionTimeout']).toBeDefined();

// Clean up the timeout to avoid test pollution
clearTimeout(transport['_reconnectionTimeout']);
});
});

describe('prevent infinite recursion when server returns 401 after successful auth', () => {
it('should throw error when server returns 401 after successful auth', async () => {
const message: JSONRPCMessage = {
Expand Down
2 changes: 1 addition & 1 deletion src/client/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export class StreamableHTTPClientTransport implements Transport {
const maxRetries = this._reconnectionOptions.maxRetries;

// Check if we've exceeded maximum retry attempts
if (maxRetries > 0 && attemptCount >= maxRetries) {
if (attemptCount >= maxRetries) {
this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));
return;
}
Expand Down
Loading