{"record":{"id":"489c5dd8ebe6f699","repo":"yikart/AiToEarn","slug":"failed-to-refresh-access-token-489c5d","errorCode":null,"errorMessage":"Failed to refresh access token","messagePattern":"Failed to refresh access token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"project/aitoearn-electron/server/src/modules/plat/youtube/youtube.auth.service.ts","lineNumber":210,"sourceCode":"      );\n\n      const TokenInfo = {\n        phone: userInfo?.phone ?? '', // 如果 userInfo.phone 为 undefined 或 null，则使用空字符串\n        id: userId,\n        name: userInfo.name,\n        isManager: false,\n        googleId: userInfo?.googleAccount?.googleId ?? ''\n      }\n      console.log(\"发送获取systemToken的info---\", TokenInfo);\n      const systemToken = await this.AuthService.generateToken(TokenInfo)\n\n      const returnRes = {url: systemToken};\n      return returnRes;\n      // 返回新的 access token 和其他信息\n      // return response.data;  // 包含新的 access_token、expires_in、token_type 等信息\n    } catch (err) {\n      console.log('Error while refreshing access token', err);\n      throw new Error('Failed to refresh access token');\n    }\n  }\n\n  /**\n   * 验证并保存授权码\n   * @param code 授权码\n   * @param state 状态码\n   * @returns 系统令牌\n   */\n  async handleAuthorizationCode(code: string, state: string, userId: string) {\n    try {\n      // 获取state关联的邮箱信息\n      const stateInfo = await this.redisService.get(`youtube:state:${userId}:${state}`);\n      if (!stateInfo || !stateInfo.mail) {\n        throw new Error('无效的状态码');\n      }\n\n      // 使用授权码获取访问令牌和刷新令牌","sourceCodeStart":192,"sourceCodeEnd":228,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/youtube/youtube.auth.service.ts#L192-L228","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Log error.response?.data from the Axios failure to see Google's exact reason (invalid_grant vs invalid_client).","Re-authorize the YouTube account with prompt=consent to obtain a fresh refresh_token when invalid_grant occurs.","Verify GOOGLE_CONFIG.WEB_CLIENT_ID and WEB_CLIENT_SECRET match the OAuth client used for authorization.","Fix getUserAccessToken to handle a null accountTokenInfo before calling refreshAccessToken.","Publish the Google OAuth app (or keep tokens fresh) to avoid 7-day testing-mode token expiry."],"exampleFix":"// before\n} catch (err) {\n  console.log('Error while refreshing access token', err);\n  throw new Error('Failed to refresh access token');\n}\n// after\n} catch (err) {\n  const reason = err?.response?.data?.error || err.message\n  console.log('Error while refreshing access token:', reason)\n  throw new Error(`Failed to refresh access token: ${reason}`)\n}","handlingStrategy":"retry","validationCode":"const token = await AccountTokenModel.findOne({ accountId })\nif (!token?.refreshToken) throw new Error('no refresh token stored; user must re-authorize with prompt=consent')","typeGuard":"const isInvalidGrant = (e: unknown): boolean =>\n  (e as any)?.response?.data?.error === 'invalid_grant' ||\n  /invalid_grant|invalid_client/i.test(String((e as any)?.message))","tryCatchPattern":"try {\n  await youtubeAuthService.refreshAccessToken(userId, accountId, refreshToken)\n} catch (e) {\n  if (isInvalidGrant(e)) {\n    await markAccountNeedsReauth(accountId) // surface 're-connect account' to the user\n    return\n  }\n  await sleep(2000) // transient network error\n  return retryOnce()\n}","preventionTips":["Always request prompt=consent&access_type=offline so a refresh_token is issued","Mark accounts as needing re-authorization instead of retrying forever on invalid_grant","Keep GOOGLE_CONFIG client id/secret in sync with the OAuth client used for consent","Publish the Google OAuth app to avoid 7-day testing-mode token expiry","Never call refreshAccessToken without checking the token record exists"],"tags":["oauth","google","token-refresh","axios"],"backgroundTag":"oauth-refresh-token-invalid-grant","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}