@@ -338,19 +338,38 @@ describe('OAuth Authorization', () => {
338338 expect ( calls . length ) . toBe ( 2 ) ;
339339 } ) ;
340340
341- it ( 'throws error on 500 status and does not fallback' , async ( ) => {
342- // First call (path-aware) returns 500
341+ it ( 'throws on 500 status without fallback' , async ( ) => {
342+ // First call (path-aware) returns 500 (overloaded server)
343343 mockFetch . mockResolvedValueOnce ( {
344344 ok : false ,
345345 status : 500
346346 } ) ;
347347
348- await expect ( discoverOAuthProtectedResourceMetadata ( 'https://resource.example.com/path/name' ) ) . rejects . toThrow ( ) ;
348+ await expect ( discoverOAuthProtectedResourceMetadata ( 'https://resource.example.com/path/name' ) ) . rejects . toThrow ( 'HTTP 500' ) ;
349349
350350 const calls = mockFetch . mock . calls ;
351351 expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
352352 } ) ;
353353
354+ it ( 'falls back to root on 502 status for path URL' , async ( ) => {
355+ // First call (path-aware) returns 502 (reverse proxy routing error)
356+ mockFetch . mockResolvedValueOnce ( {
357+ ok : false ,
358+ status : 502
359+ } ) ;
360+
361+ // Root fallback also returns 502
362+ mockFetch . mockResolvedValueOnce ( {
363+ ok : false ,
364+ status : 502
365+ } ) ;
366+
367+ await expect ( discoverOAuthProtectedResourceMetadata ( 'https://resource.example.com/path/name' ) ) . rejects . toThrow ( 'HTTP 502' ) ;
368+
369+ const calls = mockFetch . mock . calls ;
370+ expect ( calls . length ) . toBe ( 2 ) ; // Should attempt root fallback for 502
371+ } ) ;
372+
354373 it ( 'does not fallback when the original URL is already at root path' , async ( ) => {
355374 // First call (path-aware for root) returns 404
356375 mockFetch . mockResolvedValueOnce ( {
@@ -704,12 +723,54 @@ describe('OAuth Authorization', () => {
704723 expect ( metadata ) . toBeUndefined ( ) ;
705724 } ) ;
706725
707- it ( 'throws on non-404 errors' , async ( ) => {
726+ it ( 'throws on non-404 errors for root URL ' , async ( ) => {
708727 mockFetch . mockResolvedValueOnce ( new Response ( null , { status : 500 } ) ) ;
709728
710729 await expect ( discoverOAuthMetadata ( 'https://auth.example.com' ) ) . rejects . toThrow ( 'HTTP 500' ) ;
711730 } ) ;
712731
732+ it ( 'falls back to root URL on 502 for path-aware discovery' , async ( ) => {
733+ // Path-aware URL returns 502 (reverse proxy has no route for well-known path)
734+ mockFetch . mockResolvedValueOnce ( new Response ( null , { status : 502 } ) ) ;
735+
736+ // Root fallback URL succeeds
737+ mockFetch . mockResolvedValueOnce ( Response . json ( validMetadata , { status : 200 } ) ) ;
738+
739+ const metadata = await discoverOAuthMetadata ( 'https://auth.example.com/tenant1' , {
740+ authorizationServerUrl : 'https://auth.example.com/tenant1'
741+ } ) ;
742+
743+ expect ( metadata ) . toEqual ( validMetadata ) ;
744+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
745+ } ) ;
746+
747+ it ( 'does not fall back on non-502 5xx for path-aware discovery' , async ( ) => {
748+ // Path-aware URL returns 500 (overloaded server — should not retry)
749+ mockFetch . mockResolvedValueOnce ( new Response ( null , { status : 500 } ) ) ;
750+
751+ await expect (
752+ discoverOAuthMetadata ( 'https://auth.example.com/tenant1' , {
753+ authorizationServerUrl : 'https://auth.example.com/tenant1'
754+ } )
755+ ) . rejects . toThrow ( 'HTTP 500' ) ;
756+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 ) ;
757+ } ) ;
758+
759+ it ( 'throws when root fallback also returns error for path-aware discovery' , async ( ) => {
760+ // Path-aware URL returns 502 (gateway error — triggers fallback)
761+ mockFetch . mockResolvedValueOnce ( new Response ( null , { status : 502 } ) ) ;
762+
763+ // Root fallback also returns 503
764+ mockFetch . mockResolvedValueOnce ( new Response ( null , { status : 503 } ) ) ;
765+
766+ await expect (
767+ discoverOAuthMetadata ( 'https://auth.example.com/tenant1' , {
768+ authorizationServerUrl : 'https://auth.example.com/tenant1'
769+ } )
770+ ) . rejects . toThrow ( 'HTTP 503' ) ;
771+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
772+ } ) ;
773+
713774 it ( 'validates metadata schema' , async ( ) => {
714775 mockFetch . mockResolvedValueOnce (
715776 Response . json (
@@ -862,13 +923,49 @@ describe('OAuth Authorization', () => {
862923 expect ( metadata ) . toEqual ( validOpenIdMetadata ) ;
863924 } ) ;
864925
865- it ( 'throws on non-4xx errors' , async ( ) => {
926+ it ( 'continues on 502 and tries next URL' , async ( ) => {
927+ // First URL (OAuth) returns 502 (reverse proxy with no route)
866928 mockFetch . mockResolvedValueOnce ( {
867929 ok : false ,
868- status : 500
930+ status : 502 ,
931+ text : async ( ) => ''
932+ } ) ;
933+
934+ // Second URL (OIDC) succeeds
935+ mockFetch . mockResolvedValueOnce ( {
936+ ok : true ,
937+ status : 200 ,
938+ json : async ( ) => validOpenIdMetadata
939+ } ) ;
940+
941+ const metadata = await discoverAuthorizationServerMetadata ( 'https://auth.example.com' ) ;
942+
943+ expect ( metadata ) . toEqual ( validOpenIdMetadata ) ;
944+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 2 ) ;
945+ } ) ;
946+
947+ it ( 'throws on non-502 5xx errors' , async ( ) => {
948+ mockFetch . mockResolvedValueOnce ( {
949+ ok : false ,
950+ status : 500 ,
951+ text : async ( ) => ''
869952 } ) ;
870953
871- await expect ( discoverAuthorizationServerMetadata ( 'https://mcp.example.com' ) ) . rejects . toThrow ( 'HTTP 500' ) ;
954+ await expect ( discoverAuthorizationServerMetadata ( 'https://auth.example.com' ) ) . rejects . toThrow ( 'HTTP 500' ) ;
955+ expect ( mockFetch ) . toHaveBeenCalledTimes ( 1 ) ;
956+ } ) ;
957+
958+ it ( 'returns undefined when all URLs fail with 502' , async ( ) => {
959+ // All URLs return 502
960+ mockFetch . mockResolvedValue ( {
961+ ok : false ,
962+ status : 502 ,
963+ text : async ( ) => ''
964+ } ) ;
965+
966+ const metadata = await discoverAuthorizationServerMetadata ( 'https://auth.example.com/tenant1' ) ;
967+
968+ expect ( metadata ) . toBeUndefined ( ) ;
872969 } ) ;
873970
874971 it ( 'handles CORS errors with retry (browser)' , async ( ) => {
0 commit comments