yikart/AiToEarn · error · BadRequestException

交换令牌失败: ${data}

Error message

交换令牌失败: ${data}

What it means

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.

Source

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

        grant_type: 'authorization_code',
        redirect_uri: `${this.redirectUri}/api/plat/tiktok/auth/callback`,
        // 添加PKCE code_verifier
        // code_verifier: codeVerifier  // Required for mobile and desktop app only.
      });

      // const base64Credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');

      const { data } = await firstValueFrom(
        this.httpService.post(this.tokenUrl, params.toString(), {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            // 'Authorization': `Basic ${base64Credentials}`,
          }
        })
      );

      if (data.error) {
        throw new BadRequestException(`交换令牌失败: ${data}`);
      }

      // return {
      //   access_token: data.access_token,
      //   refresh_token: data.refresh_token,
      //   expires_in: data.expires_in,
      //   token_type: data.token_type,
      //   scope: data.scope,
      //   open_id: data.open_id
      // };
      return data;
    } catch (error) {
      this.logger.error('交换TikTok授权码失败:', error);
      throw new BadRequestException(`交换授权码失败: ${error.response?.data?.error_description || error.message}`);
    }
  }

  /**

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Inspect the stringified data in the message — TikTok's error/error_description pinpoints the rejected parameter.
  2. Verify redirect_uri sent in the token exchange is identical to the one used in the authorize URL and registered on the TikTok app.
  3. Check TIKTOK_CLIENT_KEY / TIKTOK_CLIENT_SECRET for the correct environment (aitoearn.cn vs aitoearn.ai).
  4. Retry the full OAuth flow with a fresh code; never reuse a consumed authorization code.

Example fix

// before
if (data.error) {
  throw new BadRequestException(`交换令牌失败: ${data}`);
}
// after
if (data.error) {
  this.logger.error(`TikTok token exchange rejected: ${data.error} - ${data.error_description}`);
  throw new BadRequestException(`交换令牌失败: ${data.error} (${data.error_description})`);
}
Defensive patterns

Strategy: validation

Validate before calling

function assertTokenExchangeParams(p: { clientKey: string; clientSecret: string; code: string; redirectUri: string }) {
  if (!p.clientKey || !p.clientSecret) throw new Error('TikTok client credentials missing');
  if (!p.code) throw new Error('Authorization code missing');
  if (!p.redirectUri) throw new Error('redirect_uri missing');
}

Type guard

function isTokenError(data: unknown): data is { error: string; error_description?: string } {
  return typeof data === 'object' && data !== null && typeof (data as any).error === 'string';
}

Try / catch

try {
  await api.exchangeTikTokCode(code);
} catch (e) {
  if (isTokenError((e as any).response?.data)) {
    const { error, error_description } = (e as any).response.data;
    if (error === 'invalid_grant') restartOAuthFlow();
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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