{"record":{"id":"1b97616ecded71a3","repo":"yikart/AiToEarn","slug":"error-response-data-error-message-1b9761","errorCode":null,"errorMessage":"上传视频失败: ${error.response?.data?.error?.message || error.message}","messagePattern":"上传视频失败: (.+?)","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"project/aitoearn-electron/server/src/modules/plat/tiktok/tiktok.service.ts","lineNumber":296,"sourceCode":"      // 使用初始化返回的上传URL，如果没有则使用默认URL\n      const uploadUrl = initData.upload_url || `${this.apiBaseUrl}/v2/video/upload/`;\n\n      const { data } = await firstValueFrom(\n        this.httpService.post(uploadUrl, formData, {\n          headers: {\n            ...formData.getHeaders(),\n            'Authorization': `Bearer ${accessToken}`\n          }\n        })\n      );\n\n      return {\n        ...data.data,\n        init_data: initData, // 返回初始化数据，可能在发布时需要\n      };\n    } catch (error) {\n      this.logger.error('上传TikTok视频失败:', error.response?.data || error.message);\n      throw new BadRequestException(`上传视频失败: ${error.response?.data?.error?.message || error.message}`);\n    }\n  }\n  \n  /**\n   * 方式1：分片上传视频（POST方式）\n   * @param accessToken 访问令牌\n   * @param videoBuffer 视频文件缓冲区\n   * @param initData 初始化返回的数据，包含 publish_id 和 upload_url\n   * @returns 上传结果\n   */\n  private async uploadVideoChunked(\n    accessToken: string,\n    videoBuffer: Buffer,\n    initData: any\n  ): Promise<any> {\n    try {\n      const { publish_id, upload_url } = initData;\n      ","sourceCodeStart":278,"sourceCodeEnd":314,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/tiktok/tiktok.service.ts#L278-L314","documentation":"This BadRequestException is thrown by TikTokService.uploadVideo's catch block when the video file upload step fails. uploadVideo either initializes first (via initVideoUpload when initData is missing), then uploads the Buffer either chunked (uploadVideoChunked for >10MB with publish_id+upload_url) or as a single multipart/form-data POST to initData.upload_url (or the default /v2/video/upload/ endpoint) with upload_params appended. Any axios/TikTok error from those calls — or any error bubbled from initVideoUpload — is logged and rethrown as '上传视频失败: <TikTok error.message or axios message>'.","triggerScenarios":"uploadVideo fails when: (1) no initData is provided and the internal initVideoUpload call throws (propagates into this catch); (2) the single-shot multipart POST to initData.upload_url returns 4xx/5xx (invalid or expired upload_url/upload_params, token expired mid-flow, wrong Content-Type/boundary handling, file exceeding size limits or not matching declared video_size); (3) uploadVideoChunked throws for large files (chunk sequence/offset errors, missing Content-Range equivalents); (4) network timeout during the binary upload.","commonSituations":"Developers hit this when the upload_url from init has expired (TikTok upload URLs are short-lived) because too much time passed between init and upload, when upload_params from init response were dropped or altered so the server rejects the POST, when the declared videoSize at init doesn't match videoBuffer.length so TikTok aborts the transfer, when accessToken expires mid-upload, and when files just over/under the 10MB threshold take the wrong (single vs chunked) path.","solutions":["Read the logged '上传TikTok视频失败:' payload for TikTok's error code; if it indicates an invalid/expired upload_url, re-run init and upload immediately after.","Ensure initData used for the upload came from the same init call for this exact file: videoBuffer.length must equal the videoSize passed to init and upload_params must be forwarded unmodified.","Verify the access token is still valid at upload time; refresh via TikTokAuthService if the init→upload gap is long.","Check FormData construction: filename/contentType (video/mp4) and formData.getHeaders() must be spread into headers so the multipart boundary is preserved.","For files >10MB confirm the chunked path sends chunks in order with correct byte ranges per TikTok's FILE_UPLOAD protocol; add retry-with-backoff for transient network failures only."],"exampleFix":"// before: single attempt, generic wrap\nconst { data } = await firstValueFrom(this.httpService.post(uploadUrl, formData, { headers: { ...formData.getHeaders(), 'Authorization': `Bearer ${accessToken}` } }));\n// after: fail fast on size mismatch before uploading, clearer error context\nif (initData?.source_info?.video_size && initData.source_info.video_size !== videoBuffer.length) {\n  throw new BadRequestException(`上传视频失败: 文件大小(${videoBuffer.length})与初始化声明(${initData.source_info.video_size})不一致`);\n}\nconst { data } = await firstValueFrom(\n  this.httpService.post(uploadUrl, formData, {\n    headers: { ...formData.getHeaders(), Authorization: `Bearer ${accessToken}` },\n    timeout: 60000,\n    maxBodyLength: Infinity,\n  })\n);","handlingStrategy":"validation","validationCode":"function assertUploadable(accessToken: string, videoBuffer: Buffer, initData?: { upload_url?: string; upload_params?: Record<string, string>; publish_id?: string }) {\n  if (!Buffer.isBuffer(videoBuffer) || videoBuffer.length === 0) throw new Error('videoBuffer 为空');\n  if (initData?.upload_url) {\n    try { new URL(initData.upload_url); } catch { throw new Error('initData.upload_url 非法'); }\n  }\n  if (initData?.source_info && initData.source_info.video_size !== videoBuffer.length) {\n    throw new Error(`文件大小不匹配: 声明 ${initData.source_info.video_size}，实际 ${videoBuffer.length}`);\n  }\n}","typeGuard":"function isUsableInitData(v: unknown): v is { publish_id: string; upload_url: string; upload_params?: Record<string, string> } {\n  return typeof v === 'object' && v !== null &&\n    typeof (v as any).upload_url === 'string' && (v as any).upload_url.length > 0;\n}","tryCatchPattern":"try {\n  const result = await tiktokService.uploadVideo(accessToken, videoBuffer);\n} catch (e) {\n  const msg = e instanceof Error ? e.message : String(e);\n  if (/expired|invalid_url/i.test(msg)) {\n    // upload_url 短时效已过期：立即重新 init 后上传\n  } else if (isAxiosUpstreamError(e) && !e.response) {\n    // 网络中断：仅对同一 initData 做幂等重试或重新 init\n  }\n  throw e;\n}","preventionTips":["Upload immediately after init — TikTok upload_url and upload_params are short-lived.","Forward initData.upload_params verbatim and spread formData.getHeaders() so the multipart boundary survives.","Make sure the videoSize declared at init equals videoBuffer.length at upload time.","Set timeout and maxBodyLength: Infinity on the binary POST so large files aren't truncated.","Use the chunked path for files >10MB when publish_id+upload_url exist; don't rely on a single multipart POST for big videos."],"tags":["tiktok","video-upload","multipart","api-error","nestjs"],"backgroundTag":"upstream-api-request-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}