jackwener/OpenCLI · error · CommandExecutionError

ImageX upload failed with status ${res.status}: ${body}

Error message

ImageX upload failed with status ${res.status}: ${body}

What it means

After PUTting the image buffer to the ImageX upload_url, imagexUpload checks res.ok and throws this CommandExecutionError with the HTTP status and response body if the upload was rejected. The body is included because ImageX error payloads usually state why (expired URL, bad content type, size limits).

Source

Thrown at clis/douyin/_shared/imagex-upload.js:50

 * @returns The store_uri (= image_uri for use in create_v2)
 */
export async function imagexUpload(imagePath, uploadInfo) {
    if (!fs.existsSync(imagePath)) {
        throw new CommandExecutionError(`Cover image file not found: ${imagePath}`, 'Ensure the file path is correct and accessible.');
    }
    const imageBuffer = fs.readFileSync(imagePath);
    const contentType = detectContentType(imagePath);
    const res = await fetch(uploadInfo.upload_url, {
        method: 'PUT',
        headers: {
            'Content-Type': contentType,
            'Content-Length': String(imageBuffer.byteLength),
        },
        body: imageBuffer,
    });
    if (!res.ok) {
        const body = await res.text().catch(() => '');
        throw new CommandExecutionError(`ImageX upload failed with status ${res.status}: ${body}`, 'Check that the upload URL is valid and has not expired.');
    }
    return uploadInfo.store_uri;
}

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Re-request the upload info (apply cover upload) to get a fresh upload_url and retry immediately.
  2. Check the error body in the message for specifics (expired signature, invalid content type, size limit).
  3. Confirm the image format/size meets ImageX requirements and convert/compress if needed.
  4. Retry once on 5xx statuses; treat 4xx as a parameter/URL problem rather than a transient failure.

Example fix

// before: reusing an old uploadInfo from a previous run
await imagexUpload(p, cachedUploadInfo);
// after: fetch fresh upload info right before upload
const uploadInfo = await applyCoverUpload(page, videoId);
await imagexUpload(p, uploadInfo);
Defensive patterns

Strategy: retry

Validate before calling

// request fresh upload info immediately before uploading
const uploadInfo = await applyCoverUpload(page); // upload_url is short-lived
if (!uploadInfo?.upload_url) throw new Error('no upload_url — apply step failed');

Type guard

null

Try / catch

try {
  return await imagexUpload(p, uploadInfo);
} catch (e) {
  if (/status 5\d\d/.test(e.message)) {
    await sleep(5000);
    return imagexUpload(p, uploadInfo); // transient — retry
  }
  if (/status 4\d\d/.test(e.message)) {
    const fresh = await applyCoverUpload(page); // likely expired URL
    return imagexUpload(p, fresh);
  }
  throw e;
}

Prevention

When it happens

Trigger: fetch PUT to uploadInfo.upload_url returns a non-2xx status: expired upload_url/token, wrong Content-Type, image too large, or ImageX service rejection.

Common situations: Delaying too long between the apply-cover-upload call and the PUT so the signed URL expires; uploading an unsupported format; network proxy intercepting the request; image exceeding size limits.

Related errors


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