yikart/AiToEarn · error · Error

Failed to refresh access token

Error message

Failed to refresh access token

What it means

refreshAccessToken POSTs to https://oauth2.googleapis.com/token with client_id, client_secret, refresh_token and grant_type=refresh_token. If the Axios call or any subsequent step fails (invalid refresh token, bad client credentials, missing user record, Redis/DB errors), it throws a generic Error('Failed to refresh access token'), hiding the Google error payload. Called by getUserAccessToken, so it breaks all authenticated YouTube API calls.

Solutions

  1. Log error.response?.data from the Axios failure to see Google's exact reason (invalid_grant vs invalid_client).
  2. Re-authorize the YouTube account with prompt=consent to obtain a fresh refresh_token when invalid_grant occurs.
  3. Verify GOOGLE_CONFIG.WEB_CLIENT_ID and WEB_CLIENT_SECRET match the OAuth client used for authorization.
  4. Fix getUserAccessToken to handle a null accountTokenInfo before calling refreshAccessToken.
  5. Publish the Google OAuth app (or keep tokens fresh) to avoid 7-day testing-mode token expiry.

Example fix

// before
} catch (err) {
  console.log('Error while refreshing access token', err);
  throw new Error('Failed to refresh access token');
}
// after
} catch (err) {
  const reason = err?.response?.data?.error || err.message
  console.log('Error while refreshing access token:', reason)
  throw new Error(`Failed to refresh access token: ${reason}`)
}
Defensive patterns

Strategy: retry

Validate before calling

const token = await AccountTokenModel.findOne({ accountId })
if (!token?.refreshToken) throw new Error('no refresh token stored; user must re-authorize with prompt=consent')

Type guard

const isInvalidGrant = (e: unknown): boolean =>
  (e as any)?.response?.data?.error === 'invalid_grant' ||
  /invalid_grant|invalid_client/i.test(String((e as any)?.message))

Try / catch

try {
  await youtubeAuthService.refreshAccessToken(userId, accountId, refreshToken)
} catch (e) {
  if (isInvalidGrant(e)) {
    await markAccountNeedsReauth(accountId) // surface 're-connect account' to the user
    return
  }
  await sleep(2000) // transient network error
  return retryOnce()
}

Prevention

When it happens

Trigger: The stored refresh_token is revoked or expired (Google returns 400 invalid_grant), GOOGLE_CONFIG.WEB_CLIENT_ID/SECRET are wrong (401 invalid_client), refreshToken is undefined because the account never consented with prompt=consent, or accountTokenInfo lookup in getUserAccessToken returned null causing a TypeError.

Common situations: Google revokes refresh tokens after 6 months of inactivity or when the user changes password; app in 'Testing' mode where tokens expire after 7 days; rotating client secrets in Google Cloud Console without updating env; re-authorizing without prompt=consent so no refresh_token is issued.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/489c5dd8ebe6f699. Report an issue: GitHub.

Appendix: source

Thrown at project/aitoearn-electron/server/src/modules/plat/youtube/youtube.auth.service.ts:210

      );

      const TokenInfo = {
        phone: userInfo?.phone ?? '', // 如果 userInfo.phone 为 undefined 或 null,则使用空字符串
        id: userId,
        name: userInfo.name,
        isManager: false,
        googleId: userInfo?.googleAccount?.googleId ?? ''
      }
      console.log("发送获取systemToken的info---", TokenInfo);
      const systemToken = await this.AuthService.generateToken(TokenInfo)

      const returnRes = {url: systemToken};
      return returnRes;
      // 返回新的 access token 和其他信息
      // return response.data;  // 包含新的 access_token、expires_in、token_type 等信息
    } catch (err) {
      console.log('Error while refreshing access token', err);
      throw new Error('Failed to refresh access token');
    }
  }

  /**
   * 验证并保存授权码
   * @param code 授权码
   * @param state 状态码
   * @returns 系统令牌
   */
  async handleAuthorizationCode(code: string, state: string, userId: string) {
    try {
      // 获取state关联的邮箱信息
      const stateInfo = await this.redisService.get(`youtube:state:${userId}:${state}`);
      if (!stateInfo || !stateInfo.mail) {
        throw new Error('无效的状态码');
      }

      // 使用授权码获取访问令牌和刷新令牌

View on GitHub (pinned to d3aa8bea5b)