{"record":{"id":"4b44680809aaeb6d","repo":"yikart/AiToEarn","slug":"error-4b4468","errorCode":null,"errorMessage":"授权失败","messagePattern":"授权失败","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"project/aitoearn-electron/server/src/modules/plat/youtube/youtube.auth.service.ts","lineNumber":325,"sourceCode":"      const results = {\n        data:\n        {\n          accountInfo: existingAccount,\n          userInfo: {\n            \"userId\": userId,\n            \"uid\": googleId\n          }\n      },\n      msg:\"success\", code: 0\n\n      };\n      console.log(\"最终返回\", results);\n\n      return results;\n\n    } catch (error) {\n      console.error('处理授权码失败:', error);\n      throw new Error('授权失败');\n    }\n  }\n\n  /**\n   * 获取YouTube频道信息并更新账号数据库\n   * @param userId 用户ID\n   * @param googleId Google ID\n   * @param accessToken 访问令牌\n   * @param refreshToken 刷新令牌\n   */\n  private async updateYouTubeAccountInfo(\n    userId: string,\n    email: string,\n    googleId: string,\n    accessToken: string,\n    refreshToken: string,\n    expires_in: number\n  ): Promise<void> {","sourceCodeStart":307,"sourceCodeEnd":343,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/youtube/youtube.auth.service.ts#L307-L343","documentation":"handleAuthorizationCode wraps its whole body in try/catch and rethrows a generic Error('授权失败') for ANY failure inside: state lookup, the token exchange POST to oauth2.googleapis.com, id_token verification (verifyIdToken with audience=webClientId), database writes, and even the earlier 无效的状态码 error. The root cause is only in console output, not in the thrown error.","triggerScenarios":"Authorization code already used or expired (Google token endpoint returns 400 invalid_grant), redirect_uri mismatch between callback request and the token exchange params, GOOGLE_CONFIG.WEB_CLIENT_SECRET wrong, verifyIdToken failing because id_token audience differs from webClientId, or the earlier state-check throw bubbling here.","commonSituations":"The web render callback URL not registered as an authorized redirect URI in Google Cloud Console; user refreshing the callback page (code reuse); environment mismatch where the token was issued to a different OAuth client; Redis state expired as in the 无效的状态码 path.","solutions":["Preserve the original error in the rethrow: throw new Error(`授权失败: ${error.message}`) so the API caller sees the cause.","Check the console '处理授权码失败' log for the Google error payload (invalid_grant, redirect_uri_mismatch, invalid_client).","Confirm GOOGLE_CONFIG.WEB_RENDER_URL-based redirect_uri exactly matches a registered redirect URI in Google Cloud Console.","Prevent callback-page refresh/replay; each authorization code is single-use.","Re-run the OAuth flow; if the state error is the cause, the fix is a longer state TTL (see 无效的状态码)."],"exampleFix":"// before\n} catch (error) {\n  console.error('处理授权码失败:', error);\n  throw new Error('授权失败');\n}\n// after\n} catch (error) {\n  console.error('处理授权码失败:', error);\n  if (error instanceof Error && error.message === '无效的状态码') throw error\n  throw new Error(`授权失败: ${error?.response?.data?.error || error.message}`)\n}","handlingStrategy":"try-catch","validationCode":"// before exchange: validate inputs and that the code is single-use fresh\nif (!code || !state || !userId) throw new Error('missing code/state/userId for authorization callback')\nconst stateInfo = await redisService.get(`youtube:state:${userId}:${state}`)\nif (!stateInfo?.mail) throw new Error('state expired')","typeGuard":"const isGoogleTokenError = (e: unknown): e is { response: { status: number; data: { error: string; error_description?: string } } } =>\n  typeof e === 'object' && e !== null &&\n  typeof (e as any).response?.data?.error === 'string'","tryCatchPattern":"try {\n  return await youtubeAuthService.handleAuthorizationCode(code, state, userId)\n} catch (e) {\n  if (isGoogleTokenError(e)) {\n    const { error, error_description } = e.response.data\n    if (error === 'invalid_grant') return restartOAuthFlow() // code used/expired\n    if (error === 'redirect_uri_mismatch') return fixRedirectUriConfig()\n  }\n  if (e.message === '无效的状态码') return restartOAuthFlow()\n  throw e\n}","preventionTips":["Register the exact redirect_uri in Google Cloud Console and keep WEB_RENDER_URL consistent","Never re-run the callback with the same authorization code (single-use)","Unwrap errors before rethrowing so logs and API responses show the root cause","Distinguish the state-check error from token-exchange errors in the catch block","Keep WEB_CLIENT_SECRET in sync with the client that initiated consent"],"tags":["oauth","google","authorization-code","error-wrapping"],"backgroundTag":"oauth-authorization-code-exchange-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}