@@ -2069,9 +2069,70 @@ export interface DeviceAuthorizationGrantPollOptions extends DPoPOptions {
20692069 signal ?: AbortSignal
20702070}
20712071
2072- function wait ( interval : number ) : Promise < void > {
2073- return new Promise ( ( resolve ) => {
2074- setTimeout ( resolve , interval * 1000 )
2072+ /**
2073+ * Gets the retry-after header and if it indicates a needed wait longer than the
2074+ * current interval, it waits for the (needed wait - current interval). Current
2075+ * interval is subtracted from the total wait here because it will be waited on
2076+ * by the subsequent poll operation anyway.
2077+ */
2078+ async function handleRetryAfter (
2079+ response : Response ,
2080+ currentInterval : number ,
2081+ signal : AbortSignal ,
2082+ throwIfInvalid = false ,
2083+ ) : Promise < void > {
2084+ const retryAfter = response . headers . get ( 'retry-after' ) ?. trim ( )
2085+ if ( retryAfter === undefined ) return
2086+
2087+ let delaySeconds : number | undefined
2088+ if ( / ^ \d + $ / . test ( retryAfter ) ) {
2089+ delaySeconds = parseInt ( retryAfter , 10 )
2090+ } else {
2091+ const retryDate = new Date ( retryAfter )
2092+ if ( Number . isFinite ( retryDate . getTime ( ) ) ) {
2093+ const now = new Date ( )
2094+ const delayMs = retryDate . getTime ( ) - now . getTime ( )
2095+ if ( delayMs > 0 ) {
2096+ delaySeconds = Math . ceil ( delayMs / 1000 )
2097+ }
2098+ }
2099+ }
2100+
2101+ if ( throwIfInvalid && ! Number . isFinite ( delaySeconds ) ) {
2102+ throw new oauth . OperationProcessingError (
2103+ 'invalid Retry-After header value' ,
2104+ { cause : response } ,
2105+ )
2106+ }
2107+
2108+ if ( delaySeconds ! > currentInterval ) {
2109+ await wait ( delaySeconds ! - currentInterval , signal )
2110+ }
2111+ }
2112+
2113+ /**
2114+ * Waits for a given duration or until an AbortSignal gets aborted
2115+ */
2116+ function wait ( duration : number , signal : AbortSignal ) : Promise < void > {
2117+ return new Promise ( ( resolve , reject ) => {
2118+ const waitStep = ( remaining : number ) => {
2119+ try {
2120+ signal . throwIfAborted ( )
2121+ } catch ( err ) {
2122+ reject ( err )
2123+ return
2124+ }
2125+
2126+ if ( remaining <= 0 ) {
2127+ resolve ( )
2128+ return
2129+ }
2130+
2131+ const currentWait = Math . min ( remaining , 5 )
2132+ setTimeout ( ( ) => waitStep ( remaining - currentWait ) , currentWait * 1000 )
2133+ }
2134+
2135+ waitStep ( duration )
20752136 } )
20762137}
20772138
@@ -2129,16 +2190,29 @@ export async function pollDeviceAuthorizationGrant(
21292190 AbortSignal . timeout ( deviceAuthorizationResponse . expires_in * 1000 )
21302191
21312192 try {
2132- pollingSignal . throwIfAborted ( )
2193+ await wait ( interval , pollingSignal )
21332194 } catch ( err ) {
21342195 errorHandler ( err )
21352196 }
21362197
2137- await wait ( interval )
2138-
21392198 const { as, c, auth, fetch, tlsOnly, nonRepudiation, timeout, decrypt } =
21402199 int ( config )
21412200
2201+ const retryPoll = ( updatedInterval : number , flag ?: typeof retry ) =>
2202+ pollDeviceAuthorizationGrant (
2203+ config ,
2204+ {
2205+ ...deviceAuthorizationResponse ,
2206+ interval : updatedInterval ,
2207+ } ,
2208+ parameters ,
2209+ {
2210+ ...options ,
2211+ signal : pollingSignal ,
2212+ flag,
2213+ } ,
2214+ )
2215+
21422216 const response = await oauth
21432217 . deviceCodeGrantRequest (
21442218 as ,
@@ -2156,6 +2230,12 @@ export async function pollDeviceAuthorizationGrant(
21562230 )
21572231 . catch ( errorHandler )
21582232
2233+ if ( response . status === 503 && response . headers . has ( 'retry-after' ) ) {
2234+ await handleRetryAfter ( response , interval , pollingSignal , true )
2235+ await response . body ?. cancel ( )
2236+ return retryPoll ( interval )
2237+ }
2238+
21592239 const p = oauth . processDeviceCodeResponse ( as , c , response , {
21602240 [ oauth . jweDecrypt ] : decrypt ,
21612241 } )
@@ -2165,19 +2245,7 @@ export async function pollDeviceAuthorizationGrant(
21652245 result = await p
21662246 } catch ( err ) {
21672247 if ( retryable ( err , options ) ) {
2168- return pollDeviceAuthorizationGrant (
2169- config ,
2170- {
2171- ...deviceAuthorizationResponse ,
2172- interval,
2173- } ,
2174- parameters ,
2175- {
2176- ...options ,
2177- signal : pollingSignal ,
2178- flag : retry ,
2179- } ,
2180- )
2248+ return retryPoll ( interval , retry )
21812249 }
21822250
21832251 if ( err instanceof oauth . ResponseBodyError ) {
@@ -2186,19 +2254,8 @@ export async function pollDeviceAuthorizationGrant(
21862254 case 'slow_down' : // Fall through
21872255 interval += 5
21882256 case 'authorization_pending' :
2189- return pollDeviceAuthorizationGrant (
2190- config ,
2191- {
2192- ...deviceAuthorizationResponse ,
2193- interval,
2194- } ,
2195- parameters ,
2196- {
2197- ...options ,
2198- signal : pollingSignal ,
2199- flag : undefined ,
2200- } ,
2201- )
2257+ await handleRetryAfter ( err . response , interval , pollingSignal )
2258+ return retryPoll ( interval )
22022259 }
22032260 }
22042261
@@ -2375,16 +2432,29 @@ export async function pollBackchannelAuthenticationGrant(
23752432 AbortSignal . timeout ( backchannelAuthenticationResponse . expires_in * 1000 )
23762433
23772434 try {
2378- pollingSignal . throwIfAborted ( )
2435+ await wait ( interval , pollingSignal )
23792436 } catch ( err ) {
23802437 errorHandler ( err )
23812438 }
23822439
2383- await wait ( interval )
2384-
23852440 const { as, c, auth, fetch, tlsOnly, nonRepudiation, timeout, decrypt } =
23862441 int ( config )
23872442
2443+ const retryPoll = ( updatedInterval : number , flag ?: typeof retry ) =>
2444+ pollBackchannelAuthenticationGrant (
2445+ config ,
2446+ {
2447+ ...backchannelAuthenticationResponse ,
2448+ interval : updatedInterval ,
2449+ } ,
2450+ parameters ,
2451+ {
2452+ ...options ,
2453+ signal : pollingSignal ,
2454+ flag,
2455+ } ,
2456+ )
2457+
23882458 const response = await oauth
23892459 . backchannelAuthenticationGrantRequest (
23902460 as ,
@@ -2402,6 +2472,12 @@ export async function pollBackchannelAuthenticationGrant(
24022472 )
24032473 . catch ( errorHandler )
24042474
2475+ if ( response . status === 503 && response . headers . has ( 'retry-after' ) ) {
2476+ await handleRetryAfter ( response , interval , pollingSignal , true )
2477+ await response . body ?. cancel ( )
2478+ return retryPoll ( interval )
2479+ }
2480+
24052481 const p = oauth . processBackchannelAuthenticationGrantResponse (
24062482 as ,
24072483 c ,
@@ -2416,19 +2492,7 @@ export async function pollBackchannelAuthenticationGrant(
24162492 result = await p
24172493 } catch ( err ) {
24182494 if ( retryable ( err , options ) ) {
2419- return pollBackchannelAuthenticationGrant (
2420- config ,
2421- {
2422- ...backchannelAuthenticationResponse ,
2423- interval,
2424- } ,
2425- parameters ,
2426- {
2427- ...options ,
2428- signal : pollingSignal ,
2429- flag : retry ,
2430- } ,
2431- )
2495+ return retryPoll ( interval , retry )
24322496 }
24332497
24342498 if ( err instanceof oauth . ResponseBodyError ) {
@@ -2437,19 +2501,8 @@ export async function pollBackchannelAuthenticationGrant(
24372501 case 'slow_down' : // Fall through
24382502 interval += 5
24392503 case 'authorization_pending' :
2440- return pollBackchannelAuthenticationGrant (
2441- config ,
2442- {
2443- ...backchannelAuthenticationResponse ,
2444- interval,
2445- } ,
2446- parameters ,
2447- {
2448- ...options ,
2449- signal : pollingSignal ,
2450- flag : undefined ,
2451- } ,
2452- )
2504+ await handleRetryAfter ( err . response , interval , pollingSignal )
2505+ return retryPoll ( interval )
24532506 }
24542507 }
24552508
0 commit comments