@@ -1008,6 +1008,78 @@ describe('StreamableHTTPClientTransport', () => {
10081008 expect ( fetchMock . mock . calls [ 0 ] ! [ 1 ] ?. method ) . toBe ( 'POST' ) ;
10091009 } ) ;
10101010
1011+ it ( 'should NOT reconnect a POST stream when error response was received' , async ( ) => {
1012+ // ARRANGE
1013+ transport = new StreamableHTTPClientTransport ( new URL ( 'http://localhost:1234/mcp' ) , {
1014+ reconnectionOptions : {
1015+ initialReconnectionDelay : 10 ,
1016+ maxRetries : 1 ,
1017+ maxReconnectionDelay : 1000 ,
1018+ reconnectionDelayGrowFactor : 1
1019+ }
1020+ } ) ;
1021+
1022+ const messageSpy = vi . fn ( ) ;
1023+ transport . onmessage = messageSpy ;
1024+
1025+ // Create a stream that sends:
1026+ // 1. Priming event with ID (enables potential reconnection)
1027+ // 2. An error response (should also prevent reconnection, just like success)
1028+ // 3. Then closes
1029+ const streamWithErrorResponse = new ReadableStream ( {
1030+ start ( controller ) {
1031+ // Priming event with ID
1032+ controller . enqueue ( new TextEncoder ( ) . encode ( 'id: priming-123\ndata: \n\n' ) ) ;
1033+ // An error response to the request (tool not found, for example)
1034+ controller . enqueue (
1035+ new TextEncoder ( ) . encode (
1036+ 'id: error-456\ndata: {"jsonrpc":"2.0","error":{"code":-32602,"message":"Tool not found"},"id":"request-1"}\n\n'
1037+ )
1038+ ) ;
1039+ // Stream closes normally
1040+ controller . close ( ) ;
1041+ }
1042+ } ) ;
1043+
1044+ const fetchMock = global . fetch as Mock ;
1045+ fetchMock . mockResolvedValueOnce ( {
1046+ ok : true ,
1047+ status : 200 ,
1048+ headers : new Headers ( { 'content-type' : 'text/event-stream' } ) ,
1049+ body : streamWithErrorResponse
1050+ } ) ;
1051+
1052+ const requestMessage : JSONRPCRequest = {
1053+ jsonrpc : '2.0' ,
1054+ method : 'tools/call' ,
1055+ id : 'request-1' ,
1056+ params : { name : 'nonexistent-tool' }
1057+ } ;
1058+
1059+ // ACT
1060+ await transport . start ( ) ;
1061+ await transport . send ( requestMessage ) ;
1062+ await vi . advanceTimersByTimeAsync ( 50 ) ;
1063+
1064+ // ASSERT
1065+ // THE KEY ASSERTION: Fetch was called ONCE only - no reconnection!
1066+ // The error response was received, so no need to reconnect.
1067+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ;
1068+ expect ( fetchMock . mock . calls [ 0 ] ! [ 1 ] ?. method ) . toBe ( 'POST' ) ;
1069+
1070+ // Verify the error response was delivered to the message handler
1071+ expect ( messageSpy ) . toHaveBeenCalledWith (
1072+ expect . objectContaining ( {
1073+ jsonrpc : '2.0' ,
1074+ error : expect . objectContaining ( {
1075+ code : - 32602 ,
1076+ message : 'Tool not found'
1077+ } ) ,
1078+ id : 'request-1'
1079+ } )
1080+ ) ;
1081+ } ) ;
1082+
10111083 it ( 'should not attempt reconnection after close() is called' , async ( ) => {
10121084 // ARRANGE
10131085 transport = new StreamableHTTPClientTransport ( new URL ( 'http://localhost:1234/mcp' ) , {
0 commit comments