{"record":{"id":"d6caec66f7e3ee4e","repo":"jackwener/OpenCLI","slug":"image-download-failed-http-response-status","errorCode":null,"errorMessage":"Image download failed: HTTP ${response.status}","messagePattern":"Image download failed: HTTP (.+?)","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"clis/twitter/utils.js","lineNumber":97,"sourceCode":" * Download a remote image to a per-call tmp directory. Returns the absolute\n * path on success. Caller owns the tmp dir and must clean it up. Throws\n * {@link ArgumentError} on bad input or download failure.\n *\n * @returns {Promise<{ absPath: string, cleanupDir: string }>}\n */\nexport async function downloadRemoteImage(imageUrl) {\n    let parsed;\n    try {\n        parsed = new URL(imageUrl);\n    } catch {\n        throw new ArgumentError(`Invalid image URL: ${imageUrl}`);\n    }\n    if (!/^https?:$/.test(parsed.protocol)) {\n        throw new ArgumentError(`Unsupported image URL protocol: ${parsed.protocol}`);\n    }\n    const response = await fetch(imageUrl);\n    if (!response.ok) {\n        throw new ArgumentError(`Image download failed: HTTP ${response.status}`);\n    }\n    const contentLength = Number(response.headers.get('content-length') || '0');\n    if (contentLength > MAX_IMAGE_SIZE_BYTES) {\n        throw new ArgumentError(`Image too large: ${(contentLength / 1024 / 1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024} MB)`);\n    }\n    const ext = resolveImageExtension(imageUrl, response.headers.get('content-type'));\n    const cleanupDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-twitter-'));\n    const absPath = path.join(cleanupDir, `image${ext}`);\n    const buffer = Buffer.from(await response.arrayBuffer());\n    if (buffer.byteLength > MAX_IMAGE_SIZE_BYTES) {\n        fs.rmSync(cleanupDir, { recursive: true, force: true });\n        throw new ArgumentError(`Image too large: ${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB (max ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024} MB)`);\n    }\n    fs.writeFileSync(absPath, buffer);\n    return { absPath, cleanupDir };\n}\n\n/**","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/utils.js#L79-L115","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Check the status code: 404 → fix the URL/confirm the image still exists; 403 → the host requires auth or blocks hotlinking; 5xx → retry later.","Regenerate expired signed URLs (fresh S3/CDN signature) before downloading.","Download the image manually (browser/curl with proper headers, e.g. a Referer or User-Agent) and pass the local file instead.","Add a retry with backoff for transient 5xx responses."],"exampleFix":"// before\nawait downloadRemoteImage('https://s3.amazonaws.com/bucket/cat.png?X-Amz-Expires=60&...'); // expired\n// after\nconst freshUrl = await getFreshPresignedUrl('bucket', 'cat.png');\nawait downloadRemoteImage(freshUrl);","handlingStrategy":"retry","validationCode":"const head = await fetch(imageUrl, { method: 'HEAD' });\nif (!head.ok) {\n  throw new Error(`Image URL not fetchable (HTTP ${head.status}); fix or refresh the URL`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await downloadRemoteImage(imageUrl);\n} catch (err) {\n  const m = err instanceof ArgumentError && err.message.match(/HTTP (\\d+)/);\n  if (m && +m[1] >= 500) return withRetry(() => downloadRemoteImage(imageUrl), 3); // transient\n  if (m && (+m[1] === 403 || +m[1] === 404)) throw new Error('URL expired or auth-walled; regenerate it');\n  throw err;\n}","preventionTips":["Regenerate presigned/CDN URLs shortly before download; they expire.","Retry with backoff only on 429/5xx, not 4xx client errors.","For auth-walled hosts, download manually with proper cookies/headers and pass a local file."],"tags":["http","network","download-failure"],"backgroundTag":"http-4xx","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}