@@ -10,6 +10,7 @@ import { randomBytes, createHash, randomUUID } from 'node:crypto';
1010import { QWEN_OAUTH_CONFIG } from '../constants.js' ;
1111import type { QwenCredentials } from '../types.js' ;
1212import { QwenAuthError , logTechnicalDetail } from '../errors.js' ;
13+ import { retryWithBackoff , getErrorStatus } from '../utils/retry.js' ;
1314
1415/**
1516 * Erro lançado quando o servidor pede slow_down (RFC 8628)
@@ -178,6 +179,7 @@ export function tokenResponseToCredentials(tokenResponse: TokenResponse): QwenCr
178179
179180/**
180181 * Refresh the access token using refresh_token grant
182+ * Includes automatic retry for transient errors (429, 5xx)
181183 */
182184export async function refreshAccessToken ( refreshToken : string ) : Promise < QwenCredentials > {
183185 const bodyData = {
@@ -186,31 +188,55 @@ export async function refreshAccessToken(refreshToken: string): Promise<QwenCred
186188 client_id : QWEN_OAUTH_CONFIG . clientId ,
187189 } ;
188190
189- const response = await fetch ( QWEN_OAUTH_CONFIG . tokenEndpoint , {
190- method : 'POST' ,
191- headers : {
192- 'Content-Type' : 'application/x-www-form-urlencoded' ,
193- Accept : 'application/json' ,
194- } ,
195- body : objectToUrlEncoded ( bodyData ) ,
196- } ) ;
197-
198- if ( ! response . ok ) {
199- const errorText = await response . text ( ) ;
200- logTechnicalDetail ( `Token refresh HTTP ${ response . status } : ${ errorText } ` ) ;
201- throw new QwenAuthError ( 'refresh_failed' , `HTTP ${ response . status } : ${ errorText } ` ) ;
202- }
191+ return retryWithBackoff (
192+ async ( ) => {
193+ const response = await fetch ( QWEN_OAUTH_CONFIG . tokenEndpoint , {
194+ method : 'POST' ,
195+ headers : {
196+ 'Content-Type' : 'application/x-www-form-urlencoded' ,
197+ Accept : 'application/json' ,
198+ } ,
199+ body : objectToUrlEncoded ( bodyData ) ,
200+ } ) ;
201+
202+ if ( ! response . ok ) {
203+ const errorText = await response . text ( ) ;
204+ logTechnicalDetail ( `Token refresh HTTP ${ response . status } : ${ errorText } ` ) ;
205+
206+ // Don't retry on invalid_grant (refresh token expired/revoked)
207+ if ( errorText . includes ( 'invalid_grant' ) ) {
208+ throw new QwenAuthError ( 'invalid_grant' , 'Refresh token expired or revoked' ) ;
209+ }
210+
211+ throw new QwenAuthError ( 'refresh_failed' , `HTTP ${ response . status } : ${ errorText } ` ) ;
212+ }
203213
204- const data = await response . json ( ) as TokenResponse ;
214+ const data = await response . json ( ) as TokenResponse ;
205215
206- return {
207- accessToken : data . access_token ,
208- tokenType : data . token_type || 'Bearer' ,
209- refreshToken : data . refresh_token || refreshToken ,
210- resourceUrl : data . resource_url ,
211- expiryDate : Date . now ( ) + data . expires_in * 1000 ,
212- scope : data . scope ,
213- } ;
216+ return {
217+ accessToken : data . access_token ,
218+ tokenType : data . token_type || 'Bearer' ,
219+ refreshToken : data . refresh_token || refreshToken ,
220+ resourceUrl : data . resource_url ,
221+ expiryDate : Date . now ( ) + data . expires_in * 1000 ,
222+ scope : data . scope ,
223+ } ;
224+ } ,
225+ {
226+ maxAttempts : 5 ,
227+ initialDelayMs : 1000 ,
228+ maxDelayMs : 15000 ,
229+ shouldRetryOnError : ( error ) => {
230+ // Don't retry on invalid_grant errors
231+ if ( error . message . includes ( 'invalid_grant' ) ) {
232+ return false ;
233+ }
234+ // Retry on 429 or 5xx errors
235+ const status = getErrorStatus ( error ) ;
236+ return status === 429 || ( status !== undefined && status >= 500 && status < 600 ) ;
237+ } ,
238+ }
239+ ) ;
214240}
215241
216242/**
0 commit comments