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

  1. 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.
  2. 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.
  3. Check the CDN/storage service status if you get 5xx statuses; retry with backoff once the CDN is healthy.
  4. 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

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


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