@@ -1001,4 +1001,69 @@ describe("StreamableHTTPClientTransport", () => {
1001
1001
expect ( global . fetch ) . not . toHaveBeenCalled ( ) ;
1002
1002
} ) ;
1003
1003
} ) ;
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
+ } ) ;
1004
1069
} ) ;
0 commit comments