@@ -193,7 +193,7 @@ describe('StreamableHTTPClientTransport', () => {
193193 await expect ( transport . terminateSession ( ) ) . resolves . not . toThrow ( ) ;
194194 } ) ;
195195
196- it ( 'should handle 404 response when session expires ' , async ( ) => {
196+ it ( 'should preserve existing 404 behavior when request is not session-bound ' , async ( ) => {
197197 const message : JSONRPCMessage = {
198198 jsonrpc : '2.0' ,
199199 method : 'test' ,
@@ -221,6 +221,63 @@ describe('StreamableHTTPClientTransport', () => {
221221 expect ( errorSpy ) . toHaveBeenCalled ( ) ;
222222 } ) ;
223223
224+ it ( 'should clear session ID and mark 404 as recoverable for session-bound POST requests' , async ( ) => {
225+ const initializeMessage : JSONRPCMessage = {
226+ jsonrpc : '2.0' ,
227+ method : 'initialize' ,
228+ params : {
229+ clientInfo : { name : 'test-client' , version : '1.0' } ,
230+ protocolVersion : '2025-03-26'
231+ } ,
232+ id : 'init-id'
233+ } ;
234+ const message : JSONRPCMessage = {
235+ jsonrpc : '2.0' ,
236+ method : 'tools/list' ,
237+ params : { } ,
238+ id : 'test-id'
239+ } ;
240+
241+ ( globalThis . fetch as Mock )
242+ . mockResolvedValueOnce ( {
243+ ok : true ,
244+ status : 202 ,
245+ headers : new Headers ( { 'mcp-session-id' : 'stale-session-id' } ) ,
246+ text : ( ) => Promise . resolve ( '' )
247+ } )
248+ . mockResolvedValueOnce ( {
249+ ok : false ,
250+ status : 404 ,
251+ statusText : 'Not Found' ,
252+ text : ( ) => Promise . resolve ( 'Session not found' ) ,
253+ headers : new Headers ( )
254+ } )
255+ . mockResolvedValueOnce ( {
256+ ok : true ,
257+ status : 202 ,
258+ headers : new Headers ( ) ,
259+ text : ( ) => Promise . resolve ( '' )
260+ } ) ;
261+
262+ await transport . send ( initializeMessage ) ;
263+ expect ( transport . sessionId ) . toBe ( 'stale-session-id' ) ;
264+
265+ await expect ( transport . send ( message ) ) . rejects . toMatchObject ( {
266+ code : SdkErrorCode . ClientHttpNotImplemented ,
267+ message : 'Session expired (HTTP 404). Cleared session ID; reconnect and re-initialize.' ,
268+ data : expect . objectContaining ( {
269+ status : 404 ,
270+ text : 'Session not found' ,
271+ sessionExpired : true
272+ } )
273+ } ) ;
274+ expect ( transport . sessionId ) . toBeUndefined ( ) ;
275+
276+ await transport . send ( { jsonrpc : '2.0' , method : 'notifications/ping' } as JSONRPCMessage ) ;
277+ const lastCall = ( globalThis . fetch as Mock ) . mock . calls . at ( - 1 ) ! ;
278+ expect ( lastCall [ 1 ] . headers . get ( 'mcp-session-id' ) ) . toBeNull ( ) ;
279+ } ) ;
280+
224281 it ( 'should handle non-streaming JSON response' , async ( ) => {
225282 const message : JSONRPCMessage = {
226283 jsonrpc : '2.0' ,
@@ -282,6 +339,38 @@ describe('StreamableHTTPClientTransport', () => {
282339 expect ( globalThis . fetch ) . toHaveBeenCalledTimes ( 2 ) ;
283340 } ) ;
284341
342+ it ( 'should clear session ID when GET SSE stream returns 404 for a session-bound request' , async ( ) => {
343+ transport = new StreamableHTTPClientTransport ( new URL ( 'http://localhost:1234/mcp' ) , {
344+ sessionId : 'stale-session-id'
345+ } ) ;
346+ await transport . start ( ) ;
347+
348+ ( globalThis . fetch as Mock ) . mockResolvedValueOnce ( {
349+ ok : false ,
350+ status : 404 ,
351+ statusText : 'Not Found' ,
352+ text : ( ) => Promise . resolve ( 'Session not found' ) ,
353+ headers : new Headers ( )
354+ } ) ;
355+
356+ await expect (
357+ ( transport as unknown as { _startOrAuthSse : ( opts : StartSSEOptions ) => Promise < void > } ) . _startOrAuthSse ( { } )
358+ ) . rejects . toMatchObject ( {
359+ code : SdkErrorCode . ClientHttpNotImplemented ,
360+ data : expect . objectContaining ( {
361+ status : 404 ,
362+ text : 'Session not found' ,
363+ sessionExpired : true
364+ } )
365+ } ) ;
366+
367+ expect ( transport . sessionId ) . toBeUndefined ( ) ;
368+
369+ const getCall = ( globalThis . fetch as Mock ) . mock . calls [ 0 ] ! ;
370+ expect ( getCall [ 1 ] . method ) . toBe ( 'GET' ) ;
371+ expect ( getCall [ 1 ] . headers . get ( 'mcp-session-id' ) ) . toBe ( 'stale-session-id' ) ;
372+ } ) ;
373+
285374 it ( 'should handle successful initial GET connection for SSE' , async ( ) => {
286375 // Set up readable stream for SSE events
287376 const encoder = new TextEncoder ( ) ;
0 commit comments