jackwener/OpenCLI · error · CommandExecutionError
HTTP ${res.status} from signed CDN URL while downloading ${i
Error message
HTTP ${res.status} from signed CDN URL while downloading ${id} What it means
This CommandExecutionError is thrown by the `attachment-download` command in clis/slock/attachment-download.js:66. The command first resolves a pre-signed CDN URL in-page (authenticated via the Slock session), then downloads the bytes with a plain Node-side `fetch(url)` that sends no auth header — the URL is pre-signed. If the CDN responds with a non-OK status (e.g. 403 or 404), the fetch itself succeeds but `res.ok` is false, so the CLI surfaces the HTTP status in this error instead of returning binary garbage.
Source
Thrown at clis/slock/attachment-download.js:66
method: 'GET',
path: `/attachments/${encodeURIComponent(id)}/url`,
serverScoped: true,
serverIdOverride: kwargs.server,
});
const result = await page.evaluate(`(async () => { ${snippet} })()`);
const rows = dispatchEvaluateResult(result);
const data = Array.isArray(rows) ? rows[0] : rows;
const url = data?.url;
if (!url) throw new CommandExecutionError(`no signed url returned for attachment ${id}`);
// Step 2 — Node side, fetch the bytes from the signed CDN URL. No auth
// header (URL is pre-signed); no Origin (Node fetch has none) so CORS
// isn't in play.
let res;
try { res = await fetch(url); }
catch (e) { throw new CommandExecutionError(`network error fetching signed URL: ${e.message}`); }
if (!res.ok) {
throw new CommandExecutionError(`HTTP ${res.status} from signed CDN URL while downloading ${id}`);
}
const ab = await res.arrayBuffer();
fs.writeFileSync(out, Buffer.from(ab));
return [{ attachmentId: id, out, sizeBytes: ab.byteLength }];
},
});
View on GitHub (pinned to 49907e53dc)
Solutions
- Re-run the command immediately after obtaining the signed URL; if it now works, the URL expired between resolution and download — download promptly after each URL fetch.
- Verify the attachmentId is a valid UUID for an existing attachment on the intended server; pass the correct `--server` slug if you operate across multiple Slock servers.
- Check the CDN/storage service status if you get 5xx statuses; retry with backoff once the CDN is healthy.
- Confirm the attachment is still attached to a message/channel — a deleted attachment yields 404 from the CDN.
Example fix
// before: resolve all URLs first, download later (URLs expire in between)
const urls = ids.map((id) => resolveUrl(page, id));
await Promise.all(urls.map((u) => download(u)));
// after: resolve and download each attachment immediately, one at a time
for (const id of ids) {
const url = resolveUrl(page, id);
await download(url, id); // use the signed URL before expiresAt
} Defensive patterns
Strategy: retry
Validate before calling
// Before trusting a resolved signed URL, check it hasn't expired
function isSignedUrlFresh(data, skewMs = 30_000) {
if (!data?.url || !data?.expiresAt) return false;
return new Date(data.expiresAt).getTime() - Date.now() > skewMs;
} Type guard
function hasSignedUrl(data) {
return typeof data === 'object' && data !== null &&
typeof data.url === 'string' && data.url.startsWith('https://');
} Try / catch
try {
const res = await fetch(url);
if (res.status === 403) {
// signature expired — re-resolve the signed URL and retry once
url = await resolveSignedUrl(page, id);
return download(url, id);
}
if (!res.ok) throw new Error(`CDN returned HTTP ${res.status} for ${id}`);
} catch (e) {
console.error(`download failed for ${id}: ${e.message}`);
} Prevention
- Download immediately after resolving the signed URL; never batch-resolve URLs for later use.
- Read `expiresAt` from the URL-resolution response and skip/re-resolve URLs within your skew margin.
- Pass the correct `--server` slug so the URL is signed for the server that owns the attachment.
- Log the HTTP status from the CDN to distinguish expiry (403) from deletion (404) and outages (5xx).
When it happens
Trigger: Running `attachment-download <attachmentId>` where the signed CDN URL fetch returns HTTP 403 (signature expired — the `expiresAt` on the URL has passed), 404 (attachment deleted or wrong id), or 5xx (CDN/storage outage). The status is whatever the CDN returned; the two-step signed-URL flow means any expiry between step 1 (URL resolution) and step 2 (download) triggers this.
Common situations: Developers script downloads slowly: the signed URL from `/api/attachments/:id/url` expires (short `expiresAt` TTL) before the Node fetch runs, e.g. after a long pause or queueing many downloads. Also hitting a 404 after the attachment was deleted, or downloading an attachment from the wrong server context (missing/incorrect `--server` slug yields a valid-looking but unauthorized URL).
Related errors
- 字幕获取失败: ${items.error}${items.text ? ' — ' + items.text : ''
- Failed to download pin ${id}: ${result.error || 'unknown err
- API_ERROR
- API_ERROR
- arXiv API HTTP ${resp.status}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/68ea6a2903f029f8.
Report an issue: GitHub.