{"record":{"id":"81e6d4d923be4ca3","repo":"jackwener/OpenCLI","slug":"tos-complete-multipart-upload-failed-with-status","errorCode":null,"errorMessage":"TOS complete multipart upload failed with status ${res.status}: ${res.body}","messagePattern":"TOS complete multipart upload failed with status (.+?): (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"critical","filePath":"clis/douyin/_shared/tos-upload.js","lineNumber":255,"sourceCode":"    const body = parts\n        .sort((a, b) => a.partNumber - b.partNumber)\n        .map(p => `${p.partNumber}:${p.crc32}`)\n        .join(',');\n    const res = await tosRequest({\n        method: 'POST',\n        url,\n        headers: gatewayHeaders(auth, uploadHeader, userId),\n        body,\n    });\n    let parsed;\n    try {\n        parsed = JSON.parse(res.body);\n    }\n    catch {\n        parsed = null;\n    }\n    if (res.status !== 200 || parsed?.code !== 2000) {\n        throw new CommandExecutionError(`TOS complete multipart upload failed with status ${res.status}: ${res.body}`, 'Check that all parts were uploaded successfully.');\n    }\n    return parsed?.data?.key || null;\n}\nlet _readSyncOverride = null;\n/** @internal — for testing only */\nexport function setReadSyncOverride(fn) {\n    _readSyncOverride = fn;\n}\n// ── Public API ───────────────────────────────────────────────────────────────\nexport async function tosUpload(options) {\n    const { filePath, uploadInfo, credentials, onProgress } = options;\n    // Validate file exists\n    if (!fs.existsSync(filePath)) {\n        throw new CommandExecutionError(`Video file not found: ${filePath}`, 'Ensure the file path is correct and accessible.');\n    }\n    const { size: fileSize } = fs.statSync(filePath);\n    if (fileSize === 0) {\n        throw new CommandExecutionError(`Video file is empty: ${filePath}`);","sourceCodeStart":237,"sourceCodeEnd":273,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/douyin/_shared/tos-upload.js#L237-L273","documentation":"completeMultipartUpload finishes the session (phase=finish) and expects HTTP 200 with parsed.code === 2000. This CommandExecutionError is thrown when the server rejects the completion, meaning the video was not finalized on TOS.","triggerScenarios":"tosUpload -> completeMultipartUpload after all parts; server returns non-200 or code != 2000, typically because some parts are missing/never accepted, or the uploadId was invalidated server-side.","commonSituations":"A part upload silently failed and the client's completedParts list disagrees with the server; resuming a session whose parts were evicted; expired auth at finish time.","solutions":["Ensure every part returned success (check completedParts covers all part numbers) before finishing","Re-acquire auth and restart the upload if the session is unrecoverable","Read res.body in the message for the server's specific rejection reason","Compare local resume state against a fresh init to detect a stale uploadId"],"exampleFix":"// before\nawait completeMultipartUpload(tosUrl, uploadId, parts, auth, uploadHeader, userId);\n// after\nconst expected = Math.ceil(fileSize / PART_SIZE);\nif (completedParts.length !== expected) {\n  throw new Error(`missing parts: have ${completedParts.length}/${expected}`);\n}\nawait completeMultipartUpload(tosUrl, uploadId, completedParts, auth, uploadHeader, userId);","handlingStrategy":"validation","validationCode":"const expectedParts = Math.ceil(fileSize / PART_SIZE);\nif (completedParts.length !== expectedParts) {\n  throw new Error(`cannot finish: ${completedParts.length}/${expectedParts} parts uploaded`);\n}","typeGuard":null,"tryCatchPattern":"try { const key = await completeMultipartUpload(tosUrl, uploadId, parts, auth, uploadHeader, userId); }\ncatch (e) {\n  if (String(e.message).startsWith('TOS complete multipart upload failed')) {\n    // parts missing server-side: restart with fresh uploadId\n    return restartUpload(filePath);\n  }\n  throw e;\n}","preventionTips":["Verify every part was accepted (count + crc32) before finishing","Use the resume state file to re-upload missing parts instead of finishing blind","Re-acquire auth if the upload spans a long time","Treat a rejected finish as unrecoverable and start a fresh session"],"tags":["upload","http","multipart","douyin"],"backgroundTag":"upload-complete-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}