jackwener/OpenCLI · critical · CommandExecutionError

TOS complete multipart upload failed with status ${res.statu

Error message

TOS complete multipart upload failed with status ${res.status}: ${res.body}

What it means

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.

Source

Thrown at clis/douyin/_shared/tos-upload.js:255

    const body = parts
        .sort((a, b) => a.partNumber - b.partNumber)
        .map(p => `${p.partNumber}:${p.crc32}`)
        .join(',');
    const res = await tosRequest({
        method: 'POST',
        url,
        headers: gatewayHeaders(auth, uploadHeader, userId),
        body,
    });
    let parsed;
    try {
        parsed = JSON.parse(res.body);
    }
    catch {
        parsed = null;
    }
    if (res.status !== 200 || parsed?.code !== 2000) {
        throw new CommandExecutionError(`TOS complete multipart upload failed with status ${res.status}: ${res.body}`, 'Check that all parts were uploaded successfully.');
    }
    return parsed?.data?.key || null;
}
let _readSyncOverride = null;
/** @internal — for testing only */
export function setReadSyncOverride(fn) {
    _readSyncOverride = fn;
}
// ── Public API ───────────────────────────────────────────────────────────────
export async function tosUpload(options) {
    const { filePath, uploadInfo, credentials, onProgress } = options;
    // Validate file exists
    if (!fs.existsSync(filePath)) {
        throw new CommandExecutionError(`Video file not found: ${filePath}`, 'Ensure the file path is correct and accessible.');
    }
    const { size: fileSize } = fs.statSync(filePath);
    if (fileSize === 0) {
        throw new CommandExecutionError(`Video file is empty: ${filePath}`);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Ensure every part returned success (check completedParts covers all part numbers) before finishing
  2. Re-acquire auth and restart the upload if the session is unrecoverable
  3. Read res.body in the message for the server's specific rejection reason
  4. Compare local resume state against a fresh init to detect a stale uploadId

Example fix

// before
await completeMultipartUpload(tosUrl, uploadId, parts, auth, uploadHeader, userId);
// after
const expected = Math.ceil(fileSize / PART_SIZE);
if (completedParts.length !== expected) {
  throw new Error(`missing parts: have ${completedParts.length}/${expected}`);
}
await completeMultipartUpload(tosUrl, uploadId, completedParts, auth, uploadHeader, userId);
Defensive patterns

Strategy: validation

Validate before calling

const expectedParts = Math.ceil(fileSize / PART_SIZE);
if (completedParts.length !== expectedParts) {
  throw new Error(`cannot finish: ${completedParts.length}/${expectedParts} parts uploaded`);
}

Try / catch

try { const key = await completeMultipartUpload(tosUrl, uploadId, parts, auth, uploadHeader, userId); }
catch (e) {
  if (String(e.message).startsWith('TOS complete multipart upload failed')) {
    // parts missing server-side: restart with fresh uploadId
    return restartUpload(filePath);
  }
  throw e;
}

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/81e6d4d923be4ca3. Report an issue: GitHub.