yikart/AiToEarn · error · BadRequestException

初始化视频发布失败: ${error.response?.data?.error?.message || error.m

Error message

初始化视频发布失败: ${error.response?.data?.error?.message || error.message}

What it means

This BadRequestException is the catch-all rethrow of TikTokService.initVideoPublish: any exception raised while calling POST ${apiBaseUrl}/v2/post/publish/video/init/ — including the missing publish_id/upload_url guard (error 421) and any axios/TikTok API error — is caught, logged, and rethrown as BadRequestException('初始化视频发布失败: <TikTok error.message or axios message>'). It deliberately overwrites the original error's identity, so the missing-field BadRequestException from line 224 also emerges with this wrapper text.

Source

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

      const { data } = await firstValueFrom(
        this.httpService.post(`${this.apiBaseUrl}/v2/post/publish/video/init/`, requestBody, {
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${accessToken}`
          }
        })
      );
      
      if (!data.data.publish_id || !data.data.upload_url) {
        throw new BadRequestException('初始化视频发布失败,缺少publish_id或upload_url');
      }

      return data.data;
    } catch (error) {
      this.logger.error('初始化TikTok视频发布失败:', error.response?.data || error.message);
      throw new BadRequestException(`初始化视频发布失败: ${error.response?.data?.error?.message || error.message}`);
    }
  }

  /**
   * 上传视频文件
   * @param accessToken 访问令牌
   * @param videoBuffer 视频文件缓冲区
   * @param initData 初始化返回的数据
   * @returns 上传结果
   */
  async uploadVideo(
    accessToken: string,
    videoBuffer: Buffer,
    initData?: any
  ): Promise<any> {
    try {
      // 如果没有提供初始化数据,先进行初始化
      if (!initData) {
        // 计算视频大小并进行初始化

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Read the logged detail ('初始化TikTok视频发布失败:' + error.response?.data) to distinguish an HTTP error from the local missing-field guard failure.
  2. If the message says 缺少publish_id或upload_url, inspect the raw TikTok response for an error envelope returned with HTTP 200 (see solutions for error 421).
  3. If it's a TikTok API error, fix per its code: refresh the access token, request/verify video.publish scope, or correct post_info/source_info fields.
  4. Validate inputs before the call: videoSize > 0, chunkSize within 5–64MB, title length within TikTok's limit, privacy_level valid for app status.
  5. Stop double-wrapping: rethrow the inner BadRequestException as-is so the specific cause ('缺少publish_id或upload_url') is not masked.

Example fix

// before: every failure becomes the same generic message
} catch (error) {
  this.logger.error('初始化TikTok视频发布失败:', error.response?.data || error.message);
  throw new BadRequestException(`初始化视频发布失败: ${error.response?.data?.error?.message || error.message}`);
}
// after: preserve already-typed errors, wrap only upstream failures
} catch (error) {
  this.logger.error('初始化TikTok视频发布失败:', error.response?.data || error.message);
  if (error instanceof BadRequestException) throw error;
  const apiErr = error.response?.data?.error;
  throw new BadRequestException(`初始化视频发布失败: [${apiErr?.code ?? 'UNKNOWN'}] ${apiErr?.message ?? error.message}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

function validatePublishInputs(accessToken: string, videoSize: number, videoInfo: { privacyStatus?: string; title?: string; tags?: string[] }) {
  if (!accessToken) throw new Error('缺少 accessToken');
  if (!Number.isFinite(videoSize) || videoSize <= 0) throw new Error('videoSize 非法');
  const privacy = videoInfo.privacyStatus ?? 'PUBLIC';
  const allowed = ['PUBLIC', 'SELF_ONLY', 'FRIENDS'];
  if (!allowed.includes(privacy)) throw new Error(`非法 privacy_level: ${privacy}`);
  const title = `${videoInfo.title ?? ''} ${(videoInfo.tags ?? []).map(t => '#' + t.replace(/^#/, '')).join(' ')}`;
  if (title.length > 2200) throw new Error('标题(含 hashtag)超出 TikTok 长度限制');
}

Type guard

function isAxiosUpstreamError(e: unknown): e is { isAxiosError: true; response?: { status: number; data: { error?: { code: string; message: string } } }; message: string } {
  return typeof e === 'object' && e !== null && (e as any).isAxiosError === true;
}

Try / catch

try {
  const initData = await tiktokService.initVideoPublish(accessToken, videoSize, videoInfo);
} catch (e) {
  if (e instanceof BadRequestException && e.message.includes('缺少publish_id')) {
    // local response-shape failure — inspect raw response, do not blindly retry
  } else if (isAxiosUpstreamError(e) && e.response?.status === 401) {
    // token invalid — refresh and retry once
  } else if (isAxiosUpstreamError(e) && !e.response) {
    // network error — retry with backoff
  }
  throw e;
}

Prevention

When it happens

Trigger: initVideoPublish's try block fails for any reason: (1) axios error from firstValueFrom (4xx/5xx from TikTok: invalid token, missing video.publish scope, invalid post_info/source_info values, file size outside limits); (2) the explicit '缺少publish_id或upload_url' BadRequestException thrown at line 224; (3) network failure where error.response is undefined so error.message (e.g. 'timeout of 10000ms exceeded') is interpolated.

Common situations: Developers hit this with expired/revoked OAuth tokens, apps whose video.publish scope isn't audit-approved, privacy_level values not permitted for the app tier, video_size/chunk_size/total_chunk_count inconsistencies in source_info, oversized title strings with hashtags exceeding TikTok limits, and confusingly also when the response genuinely lacks publish_id/upload_url because the inner guard's message gets swallowed by this wrapper.

Related errors


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