@@ -1001,4 +1001,69 @@ describe("StreamableHTTPClientTransport", () => {
10011001 expect ( global . fetch ) . not . toHaveBeenCalled ( ) ;
10021002 } ) ;
10031003 } ) ;
1004+
1005+ describe ( "prevent infinite recursion when server returns 401 after successful auth" , ( ) => {
1006+ it ( "should throw error when server returns 401 after successful auth" , async ( ) => {
1007+ const message : JSONRPCMessage = {
1008+ jsonrpc : "2.0" ,
1009+ method : "test" ,
1010+ params : { } ,
1011+ id : "test-id"
1012+ } ;
1013+
1014+ // Mock provider with refresh token to enable token refresh flow
1015+ mockAuthProvider . tokens . mockResolvedValue ( {
1016+ access_token : "test-token" ,
1017+ token_type : "Bearer" ,
1018+ refresh_token : "refresh-token" ,
1019+ } ) ;
1020+
1021+ const unauthedResponse = {
1022+ ok : false ,
1023+ status : 401 ,
1024+ statusText : "Unauthorized" ,
1025+ headers : new Headers ( )
1026+ } ;
1027+
1028+ ( global . fetch as jest . Mock )
1029+ // First request - 401, triggers auth flow
1030+ . mockResolvedValueOnce ( unauthedResponse )
1031+ // Resource discovery, path aware
1032+ . mockResolvedValueOnce ( unauthedResponse )
1033+ // Resource discovery, root
1034+ . mockResolvedValueOnce ( unauthedResponse )
1035+ // OAuth metadata discovery
1036+ . mockResolvedValueOnce ( {
1037+ ok : true ,
1038+ status : 200 ,
1039+ json : async ( ) => ( {
1040+ issuer : "http://localhost:1234" ,
1041+ authorization_endpoint : "http://localhost:1234/authorize" ,
1042+ token_endpoint : "http://localhost:1234/token" ,
1043+ response_types_supported : [ "code" ] ,
1044+ code_challenge_methods_supported : [ "S256" ] ,
1045+ } ) ,
1046+ } )
1047+ // Token refresh succeeds
1048+ . mockResolvedValueOnce ( {
1049+ ok : true ,
1050+ status : 200 ,
1051+ json : async ( ) => ( {
1052+ access_token : "new-access-token" ,
1053+ token_type : "Bearer" ,
1054+ expires_in : 3600 ,
1055+ } ) ,
1056+ } )
1057+ // Retry the original request - still 401 (broken server)
1058+ . mockResolvedValueOnce ( unauthedResponse ) ;
1059+
1060+ await expect ( transport . send ( message ) ) . rejects . toThrow ( "Server returned 401 after successful authentication" ) ;
1061+ expect ( mockAuthProvider . saveTokens ) . toHaveBeenCalledWith ( {
1062+ access_token : "new-access-token" ,
1063+ token_type : "Bearer" ,
1064+ expires_in : 3600 ,
1065+ refresh_token : "refresh-token" , // Refresh token is preserved
1066+ } ) ;
1067+ } ) ;
1068+ } ) ;
10041069} ) ;
0 commit comments