@@ -52,6 +52,10 @@ function createKnownErrorResponse(error: InstanceType<typeof KnownErrors[keyof t
5252 } ) ;
5353}
5454
55+ function createTextResponse ( body : string , options : { status : number , headers ?: Record < string , string > } ) : Response {
56+ return new Response ( body , options ) ;
57+ }
58+
5559function getRequestBody ( fetchMock : { mock : { calls : unknown [ ] [ ] } } ) : Record < string , unknown > {
5660 const requestInit = fetchMock . mock . calls [ 0 ] ?. [ 1 ] ;
5761 if ( requestInit == null || typeof requestInit !== "object" || ! ( "body" in requestInit ) ) {
@@ -437,6 +441,63 @@ describe("_withFallback", () => {
437441 expect ( log . every ( u => urlIndex ( urls , u ) === 0 ) ) . toBe ( true ) ;
438442 } ) ;
439443
444+ it ( "does not retry or fall back on non-KnownError 4xx responses" , async ( ) => {
445+ const urls = urlList ( 3 ) ;
446+ const log : string [ ] = [ ] ;
447+ vi . stubGlobal ( "fetch" , vi . fn ( async ( input : RequestInfo | URL ) => {
448+ log . push ( input . toString ( ) ) ;
449+ return createTextResponse ( "Payments are not set up" , { status : 402 } ) ;
450+ } ) ) ;
451+
452+ const iface = createClientInterface ( { apiUrls : urls } ) ;
453+ await expect ( sendRequest ( iface ) ) . rejects . toMatchObject ( { name : "Error" } ) ;
454+ expect ( log . length ) . toBe ( 1 ) ;
455+ expect ( urlIndex ( urls , log [ 0 ] ) ) . toBe ( 0 ) ;
456+ } ) ;
457+
458+ it ( "wraps non-KnownError 4xx responses as normal errors" , async ( ) => {
459+ const response = createTextResponse ( "Payments are not set up" , { status : 402 } ) ;
460+ vi . stubGlobal ( "fetch" , vi . fn ( async ( ) => response ) ) ;
461+
462+ const iface = createClientInterface ( { apiUrls : urlList ( 1 ) } ) ;
463+ await expect ( sendRequest ( iface ) ) . rejects . toMatchObject ( {
464+ name : "Error" ,
465+ message : expect . stringContaining ( "402 Payments are not set up" ) ,
466+ cause : response ,
467+ } ) ;
468+ } ) ;
469+
470+ it ( "does not retry non-KnownError 5xx responses on a single URL" , async ( ) => {
471+ let attempts = 0 ;
472+ vi . stubGlobal ( "fetch" , vi . fn ( async ( ) => {
473+ attempts ++ ;
474+ return createTextResponse ( "Server unavailable" , { status : 503 } ) ;
475+ } ) ) ;
476+
477+ const iface = createClientInterface ( { apiUrls : urlList ( 1 ) } ) ;
478+ await expect ( sendRequest ( iface ) ) . rejects . toThrow ( "503 Server unavailable" ) ;
479+ expect ( attempts ) . toBe ( 1 ) ;
480+ } ) ;
481+
482+ it ( "falls back on non-KnownError 5xx responses" , async ( ) => {
483+ const urls = urlList ( 3 ) ;
484+ const log : string [ ] = [ ] ;
485+ vi . stubGlobal ( "fetch" , vi . fn ( async ( input : RequestInfo | URL ) => {
486+ const url = input . toString ( ) ;
487+ log . push ( url ) ;
488+ if ( urlIndex ( urls , url ) === 0 ) {
489+ return createTextResponse ( "Server unavailable" , { status : 503 } ) ;
490+ }
491+ return createJsonResponse ( { display_name : "test" } ) ;
492+ } ) ) ;
493+
494+ const iface = createClientInterface ( { apiUrls : urls } ) ;
495+ await sendRequest ( iface ) ;
496+ expect ( log . length ) . toBe ( 2 ) ;
497+ expect ( urlIndex ( urls , log [ 0 ] ) ) . toBe ( 0 ) ;
498+ expect ( urlIndex ( urls , log [ 1 ] ) ) . toBe ( 1 ) ;
499+ } ) ;
500+
440501 it ( "makes 2 passes × N URLs attempts before throwing" , async ( ) => {
441502 for ( const n of [ 2 , 3 , 5 ] ) {
442503 const urls = urlList ( n ) ;
0 commit comments