jackwener/OpenCLI · error · ArgumentError

Image download failed: HTTP ${response.status}

Error message

Image download failed: HTTP ${response.status}

What it means

After fetching the remote image, downloadRemoteImage checks response.ok; any non-2xx HTTP status (404 Not Found, 403 Forbidden, 410 Gone, 5xx) throws ArgumentError `Image download failed: HTTP <status>`. The URL was valid but the server refused or could not serve the image.

Source

Thrown at clis/twitter/utils.js:97

 * Download a remote image to a per-call tmp directory. Returns the absolute
 * path on success. Caller owns the tmp dir and must clean it up. Throws
 * {@link ArgumentError} on bad input or download failure.
 *
 * @returns {Promise<{ absPath: string, cleanupDir: string }>}
 */
export async function downloadRemoteImage(imageUrl) {
    let parsed;
    try {
        parsed = new URL(imageUrl);
    } catch {
        throw new ArgumentError(`Invalid image URL: ${imageUrl}`);
    }
    if (!/^https?:$/.test(parsed.protocol)) {
        throw new ArgumentError(`Unsupported image URL protocol: ${parsed.protocol}`);
    }
    const response = await fetch(imageUrl);
    if (!response.ok) {
        throw new ArgumentError(`Image download failed: HTTP ${response.status}`);
    }
    const contentLength = Number(response.headers.get('content-length') || '0');
    if (contentLength > MAX_IMAGE_SIZE_BYTES) {
        throw new ArgumentError(`Image too large: ${(contentLength / 1024 / 1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024} MB)`);
    }
    const ext = resolveImageExtension(imageUrl, response.headers.get('content-type'));
    const cleanupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-twitter-'));
    const absPath = path.join(cleanupDir, `image${ext}`);
    const buffer = Buffer.from(await response.arrayBuffer());
    if (buffer.byteLength > MAX_IMAGE_SIZE_BYTES) {
        fs.rmSync(cleanupDir, { recursive: true, force: true });
        throw new ArgumentError(`Image too large: ${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024} MB)`);
    }
    fs.writeFileSync(absPath, buffer);
    return { absPath, cleanupDir };
}

/**

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Check the status code: 404 → fix the URL/confirm the image still exists; 403 → the host requires auth or blocks hotlinking; 5xx → retry later.
  2. Regenerate expired signed URLs (fresh S3/CDN signature) before downloading.
  3. Download the image manually (browser/curl with proper headers, e.g. a Referer or User-Agent) and pass the local file instead.
  4. Add a retry with backoff for transient 5xx responses.

Example fix

// before
await downloadRemoteImage('https://s3.amazonaws.com/bucket/cat.png?X-Amz-Expires=60&...'); // expired
// after
const freshUrl = await getFreshPresignedUrl('bucket', 'cat.png');
await downloadRemoteImage(freshUrl);
Defensive patterns

Strategy: retry

Validate before calling

const head = await fetch(imageUrl, { method: 'HEAD' });
if (!head.ok) {
  throw new Error(`Image URL not fetchable (HTTP ${head.status}); fix or refresh the URL`);
}

Try / catch

try {
  await downloadRemoteImage(imageUrl);
} catch (err) {
  const m = err instanceof ArgumentError && err.message.match(/HTTP (\d+)/);
  if (m && +m[1] >= 500) return withRetry(() => downloadRemoteImage(imageUrl), 3); // transient
  if (m && (+m[1] === 403 || +m[1] === 404)) throw new Error('URL expired or auth-walled; regenerate it');
  throw err;
}

Prevention

When it happens

Trigger: fetch() returns a 4xx/5xx for the image URL: deleted or renamed asset, expired signed/CDN URL (403), hotlink protection, auth-walled image host, or origin/server error (5xx).

Common situations: S3/CloudFront presigned URLs that expired; images behind logins (Instagram/private hosts); Instagram/Twitter CDN links that rotate; transient 502/503 from the image host.

Related errors


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