{"record":{"id":"9c01723995c2bcac","repo":"yikart/AiToEarn","slug":"data","errorCode":null,"errorMessage":"交换令牌失败: ${data}","messagePattern":"交换令牌失败: (.+?)","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"project/aitoearn-electron/server/src/modules/plat/tiktok/tiktok.auth.service.ts","lineNumber":280,"sourceCode":"        grant_type: 'authorization_code',\n        redirect_uri: `${this.redirectUri}/api/plat/tiktok/auth/callback`,\n        // 添加PKCE code_verifier\n        // code_verifier: codeVerifier  // Required for mobile and desktop app only.\n      });\n\n      // const base64Credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');\n\n      const { data } = await firstValueFrom(\n        this.httpService.post(this.tokenUrl, params.toString(), {\n          headers: {\n            'Content-Type': 'application/x-www-form-urlencoded',\n            // 'Authorization': `Basic ${base64Credentials}`,\n          }\n        })\n      );\n\n      if (data.error) {\n        throw new BadRequestException(`交换令牌失败: ${data}`);\n      }\n\n      // return {\n      //   access_token: data.access_token,\n      //   refresh_token: data.refresh_token,\n      //   expires_in: data.expires_in,\n      //   token_type: data.token_type,\n      //   scope: data.scope,\n      //   open_id: data.open_id\n      // };\n      return data;\n    } catch (error) {\n      this.logger.error('交换TikTok授权码失败:', error);\n      throw new BadRequestException(`交换授权码失败: ${error.response?.data?.error_description || error.message}`);\n    }\n  }\n\n  /**","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/tiktok/tiktok.auth.service.ts#L262-L298","documentation":"A BadRequestException raised inside exchangeCodeForTokens when TikTok's OAuth token endpoint responds with HTTP 200 but the JSON body contains an error field. The whole response body is stringified into the message, so the raw TikTok error payload (error, error_description, error_uri) is visible to the caller.","triggerScenarios":"POST to https://open.tiktokapis.com/v2/oauth/token/ returns { error: ... } because client_key/client_secret is wrong, redirect_uri doesn't match the registered one, the code was already consumed or expired, or grant_type/parameters are malformed.","commonSituations":"Wrong redirect_uri (must match byte-for-byte), swapped client key/secret, code re-use after page refresh of the callback URL, using a CN-environment key against the international endpoint or vice versa.","solutions":["Inspect the stringified data in the message — TikTok's error/error_description pinpoints the rejected parameter.","Verify redirect_uri sent in the token exchange is identical to the one used in the authorize URL and registered on the TikTok app.","Check TIKTOK_CLIENT_KEY / TIKTOK_CLIENT_SECRET for the correct environment (aitoearn.cn vs aitoearn.ai).","Retry the full OAuth flow with a fresh code; never reuse a consumed authorization code."],"exampleFix":"// before\nif (data.error) {\n  throw new BadRequestException(`交换令牌失败: ${data}`);\n}\n// after\nif (data.error) {\n  this.logger.error(`TikTok token exchange rejected: ${data.error} - ${data.error_description}`);\n  throw new BadRequestException(`交换令牌失败: ${data.error} (${data.error_description})`);\n}","handlingStrategy":"validation","validationCode":"function assertTokenExchangeParams(p: { clientKey: string; clientSecret: string; code: string; redirectUri: string }) {\n  if (!p.clientKey || !p.clientSecret) throw new Error('TikTok client credentials missing');\n  if (!p.code) throw new Error('Authorization code missing');\n  if (!p.redirectUri) throw new Error('redirect_uri missing');\n}","typeGuard":"function isTokenError(data: unknown): data is { error: string; error_description?: string } {\n  return typeof data === 'object' && data !== null && typeof (data as any).error === 'string';\n}","tryCatchPattern":"try {\n  await api.exchangeTikTokCode(code);\n} catch (e) {\n  if (isTokenError((e as any).response?.data)) {\n    const { error, error_description } = (e as any).response.data;\n    if (error === 'invalid_grant') restartOAuthFlow();\n  }\n  throw e;\n}","preventionTips":["Compare redirect_uri in the token request byte-for-byte with the authorize URL and app registration.","Validate client_key/client_secret at startup, not at request time.","Consume the code exactly once, immediately after redirect.","Log the full TikTok error body before wrapping."],"tags":["oauth","tiktok","token-exchange","bad-request"],"backgroundTag":"oauth-token-exchange-rejected","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}