@@ -220,7 +220,7 @@ describe('StreamableHTTPClientTransport', () => {
220220 await expect ( transport . terminateSession ( ) ) . resolves . not . toThrow ( ) ;
221221 } ) ;
222222
223- it ( 'should handle 404 response when session expires ' , async ( ) => {
223+ it ( 'should preserve existing 404 behavior when request is not session-bound ' , async ( ) => {
224224 const message : JSONRPCMessage = {
225225 jsonrpc : '2.0' ,
226226 method : 'test' ,
@@ -248,6 +248,63 @@ describe('StreamableHTTPClientTransport', () => {
248248 expect ( errorSpy ) . toHaveBeenCalled ( ) ;
249249 } ) ;
250250
251+ it ( 'should clear session ID and mark 404 as recoverable for session-bound POST requests' , async ( ) => {
252+ const initializeMessage : JSONRPCMessage = {
253+ jsonrpc : '2.0' ,
254+ method : 'initialize' ,
255+ params : {
256+ clientInfo : { name : 'test-client' , version : '1.0' } ,
257+ protocolVersion : '2025-03-26'
258+ } ,
259+ id : 'init-id'
260+ } ;
261+ const message : JSONRPCMessage = {
262+ jsonrpc : '2.0' ,
263+ method : 'tools/list' ,
264+ params : { } ,
265+ id : 'test-id'
266+ } ;
267+
268+ ( globalThis . fetch as Mock )
269+ . mockResolvedValueOnce ( {
270+ ok : true ,
271+ status : 202 ,
272+ headers : new Headers ( { 'mcp-session-id' : 'stale-session-id' } ) ,
273+ text : ( ) => Promise . resolve ( '' )
274+ } )
275+ . mockResolvedValueOnce ( {
276+ ok : false ,
277+ status : 404 ,
278+ statusText : 'Not Found' ,
279+ text : ( ) => Promise . resolve ( 'Session not found' ) ,
280+ headers : new Headers ( )
281+ } )
282+ . mockResolvedValueOnce ( {
283+ ok : true ,
284+ status : 202 ,
285+ headers : new Headers ( ) ,
286+ text : ( ) => Promise . resolve ( '' )
287+ } ) ;
288+
289+ await transport . send ( initializeMessage ) ;
290+ expect ( transport . sessionId ) . toBe ( 'stale-session-id' ) ;
291+
292+ await expect ( transport . send ( message ) ) . rejects . toMatchObject ( {
293+ code : SdkErrorCode . ClientHttpNotImplemented ,
294+ message : 'Session expired (HTTP 404). Cleared session ID; reconnect and re-initialize.' ,
295+ data : expect . objectContaining ( {
296+ status : 404 ,
297+ text : 'Session not found' ,
298+ sessionExpired : true
299+ } )
300+ } ) ;
301+ expect ( transport . sessionId ) . toBeUndefined ( ) ;
302+
303+ await transport . send ( { jsonrpc : '2.0' , method : 'notifications/ping' } as JSONRPCMessage ) ;
304+ const lastCall = ( globalThis . fetch as Mock ) . mock . calls . at ( - 1 ) ! ;
305+ expect ( lastCall [ 1 ] . headers . get ( 'mcp-session-id' ) ) . toBeNull ( ) ;
306+ } ) ;
307+
251308 it ( 'should handle non-streaming JSON response' , async ( ) => {
252309 const message : JSONRPCMessage = {
253310 jsonrpc : '2.0' ,
@@ -309,6 +366,38 @@ describe('StreamableHTTPClientTransport', () => {
309366 expect ( globalThis . fetch ) . toHaveBeenCalledTimes ( 2 ) ;
310367 } ) ;
311368
369+ it ( 'should clear session ID when GET SSE stream returns 404 for a session-bound request' , async ( ) => {
370+ transport = new StreamableHTTPClientTransport ( new URL ( 'http://localhost:1234/mcp' ) , {
371+ sessionId : 'stale-session-id'
372+ } ) ;
373+ await transport . start ( ) ;
374+
375+ ( globalThis . fetch as Mock ) . mockResolvedValueOnce ( {
376+ ok : false ,
377+ status : 404 ,
378+ statusText : 'Not Found' ,
379+ text : ( ) => Promise . resolve ( 'Session not found' ) ,
380+ headers : new Headers ( )
381+ } ) ;
382+
383+ await expect (
384+ ( transport as unknown as { _startOrAuthSse : ( opts : StartSSEOptions ) => Promise < void > } ) . _startOrAuthSse ( { } )
385+ ) . rejects . toMatchObject ( {
386+ code : SdkErrorCode . ClientHttpNotImplemented ,
387+ data : expect . objectContaining ( {
388+ status : 404 ,
389+ text : 'Session not found' ,
390+ sessionExpired : true
391+ } )
392+ } ) ;
393+
394+ expect ( transport . sessionId ) . toBeUndefined ( ) ;
395+
396+ const getCall = ( globalThis . fetch as Mock ) . mock . calls [ 0 ] ! ;
397+ expect ( getCall [ 1 ] . method ) . toBe ( 'GET' ) ;
398+ expect ( getCall [ 1 ] . headers . get ( 'mcp-session-id' ) ) . toBe ( 'stale-session-id' ) ;
399+ } ) ;
400+
312401 it ( 'should handle successful initial GET connection for SSE' , async ( ) => {
313402 // Set up readable stream for SSE events
314403 const encoder = new TextEncoder ( ) ;
0 commit comments