@@ -7,6 +7,8 @@ import {ExposedLoggers, Utils, withLogContext} from "./logging";
77// eslint-disable-next-line @typescript-eslint/no-unused-vars
88import { context } from "./context" ;
99import { Context } from "./context" ;
10+ import retry , { RetriableError } from "./retries/retries" ;
11+ import Time , { TimeUnits } from "./retries/Time" ;
1012
1113// eslint-disable-next-line @typescript-eslint/no-var-requires
1214const sdkVersion = require ( "../package.json" ) . version ;
@@ -109,38 +111,75 @@ export class ApiClient {
109111 }
110112 }
111113
112- let response ;
114+ const response = await retry <
115+ Awaited < Awaited < ReturnType < typeof fetch > > [ "response" ] >
116+ > ( {
117+ timeout : new Time ( 10 , TimeUnits . seconds ) ,
118+ fn : async ( ) => {
119+ let response ;
120+ try {
121+ const { abort, response : responsePromise } = await fetch (
122+ url . toString ( ) ,
123+ options
124+ ) ;
125+ if ( context ?. cancellationToken ?. onCancellationRequested ) {
126+ context ?. cancellationToken ?. onCancellationRequested (
127+ abort
128+ ) ;
129+ }
130+ response = await responsePromise ;
131+ } catch ( e : any ) {
132+ const err =
133+ e . code && e . code === "ENOTFOUND"
134+ ? new HttpError (
135+ `Can't connect to ${ url . toString ( ) } ` ,
136+ 500
137+ )
138+ : e ;
139+ throw logAndReturnError ( url , options , "" , err , context ) ;
140+ }
141+
142+ switch ( response . status ) {
143+ case 500 :
144+ case 429 :
145+ throw new RetriableError ( ) ;
146+
147+ default :
148+ break ;
149+ }
150+ return response ;
151+ } ,
152+ } ) ;
113153
154+ let responseText ! : string ;
114155 try {
115- const { abort, response : responsePromise } = await fetch (
116- url . toString ( ) ,
117- options
118- ) ;
119- if ( context ?. cancellationToken ?. onCancellationRequested ) {
120- context ?. cancellationToken ?. onCancellationRequested ( abort ) ;
121- }
122- response = await responsePromise ;
156+ const responseBody = await response . arrayBuffer ( ) ;
157+ responseText = new TextDecoder ( ) . decode ( responseBody ) ;
123158 } catch ( e : any ) {
124- const err =
125- e . code && e . code === "ENOTFOUND"
126- ? new HttpError ( `Can't connect to ${ url . toString ( ) } ` , 500 )
127- : e ;
128- throw logAndReturnError ( url , options , response , err , context ) ;
159+ logAndReturnError ( url , options , "" , e , context ) ;
160+ throw new ApiClientResponseError (
161+ `Can't parse response from ${ url . toString ( ) } ` ,
162+ ""
163+ ) ;
129164 }
130165
131166 // throw error if the URL is incorrect and we get back an HTML page
132167 if ( response . headers . get ( "content-type" ) ?. match ( "text/html" ) ) {
133- throw logAndReturnError (
134- url ,
135- options ,
136- response ,
137- new HttpError ( `Can't connect to ${ url . toString ( ) } ` , 404 ) ,
138- context
139- ) ;
140- }
168+ // When the AAD tenent is not configured correctly, the response is a HTML page with a title like this:
169+ // "Error 400 io.jsonwebtoken.IncorrectClaimException: Expected iss claim to be: https://sts.windows.net/aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaa/, but was: https://sts.windows.net/bbbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbb/."
170+ const m = responseText . match ( / < t i t l e > ( E r r o r \d + .* ?) < \/ t i t l e > / ) ;
171+ let error : HttpError ;
172+ if ( m ) {
173+ error = new HttpError ( m [ 1 ] , response . status ) ;
174+ } else {
175+ error = new HttpError (
176+ `Can't connect to ${ url . toString ( ) } ` ,
177+ response . status
178+ ) ;
179+ }
141180
142- const responseBody = await response . arrayBuffer ( ) ;
143- const responseText = new TextDecoder ( ) . decode ( responseBody ) ;
181+ throw logAndReturnError ( url , options , response , error , context ) ;
182+ }
144183
145184 // TODO proper error handling
146185 if ( ! response . ok ) {
@@ -150,33 +189,40 @@ export class ApiClient {
150189 throw logAndReturnError ( url , options , responseText , err , context ) ;
151190 }
152191
192+ let responseJson : any ;
153193 try {
154- response = JSON . parse ( responseText ) ;
194+ responseJson = JSON . parse ( responseText ) ;
155195 } catch ( e ) {
156196 logAndReturnError ( url , options , responseText , e , context ) ;
157- new ApiClientResponseError ( responseText , response ) ;
197+ throw new ApiClientResponseError ( responseText , responseJson ) ;
158198 }
159199
160- if ( "error" in response ) {
161- logAndReturnError ( url , options , response , response . error , context ) ;
162- throw new ApiClientResponseError ( response . error , response ) ;
200+ if ( "error" in responseJson ) {
201+ logAndReturnError (
202+ url ,
203+ options ,
204+ responseJson ,
205+ responseJson . error ,
206+ context
207+ ) ;
208+ throw new ApiClientResponseError ( responseJson . error , responseJson ) ;
163209 }
164210
165- if ( "error_code" in response ) {
211+ if ( "error_code" in responseJson ) {
166212 const message =
167- response . message || `HTTP error ${ response . error_code } ` ;
213+ responseJson . message || `HTTP error ${ responseJson . error_code } ` ;
168214 throw logAndReturnError (
169215 url ,
170216 options ,
171- response ,
172- new HttpError ( message , response . error_code ) ,
217+ responseJson ,
218+ new HttpError ( message , responseJson . error_code ) ,
173219 context
174220 ) ;
175221 }
176222 context ?. logger ?. debug ( url . toString ( ) , {
177223 request : options ,
178- response : response ,
224+ response : responseJson ,
179225 } ) ;
180- return response as any ;
226+ return responseJson ;
181227 }
182228}
0 commit comments