{"record":{"id":"68ea6a2903f029f8","repo":"jackwener/OpenCLI","slug":"http-res-status-from-signed-cdn-url-while-downl","errorCode":null,"errorMessage":"HTTP ${res.status} from signed CDN URL while downloading ${id}","messagePattern":"HTTP (.+?) from signed CDN URL while downloading (.+?)","errorType":"http","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/slock/attachment-download.js","lineNumber":66,"sourceCode":"      method: 'GET',\n      path: `/attachments/${encodeURIComponent(id)}/url`,\n      serverScoped: true,\n      serverIdOverride: kwargs.server,\n    });\n    const result = await page.evaluate(`(async () => { ${snippet} })()`);\n    const rows = dispatchEvaluateResult(result);\n    const data = Array.isArray(rows) ? rows[0] : rows;\n    const url = data?.url;\n    if (!url) throw new CommandExecutionError(`no signed url returned for attachment ${id}`);\n\n    // Step 2 — Node side, fetch the bytes from the signed CDN URL. No auth\n    // header (URL is pre-signed); no Origin (Node fetch has none) so CORS\n    // isn't in play.\n    let res;\n    try { res = await fetch(url); }\n    catch (e) { throw new CommandExecutionError(`network error fetching signed URL: ${e.message}`); }\n    if (!res.ok) {\n      throw new CommandExecutionError(`HTTP ${res.status} from signed CDN URL while downloading ${id}`);\n    }\n    const ab = await res.arrayBuffer();\n    fs.writeFileSync(out, Buffer.from(ab));\n    return [{ attachmentId: id, out, sizeBytes: ab.byteLength }];\n  },\n});\n","sourceCodeStart":48,"sourceCodeEnd":73,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/slock/attachment-download.js#L48-L73","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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."],"exampleFix":"// before: resolve all URLs first, download later (URLs expire in between)\nconst urls = ids.map((id) => resolveUrl(page, id));\nawait Promise.all(urls.map((u) => download(u)));\n// after: resolve and download each attachment immediately, one at a time\nfor (const id of ids) {\n  const url = resolveUrl(page, id);\n  await download(url, id); // use the signed URL before expiresAt\n}","handlingStrategy":"retry","validationCode":"// Before trusting a resolved signed URL, check it hasn't expired\nfunction isSignedUrlFresh(data, skewMs = 30_000) {\n  if (!data?.url || !data?.expiresAt) return false;\n  return new Date(data.expiresAt).getTime() - Date.now() > skewMs;\n}","typeGuard":"function hasSignedUrl(data) {\n  return typeof data === 'object' && data !== null &&\n    typeof data.url === 'string' && data.url.startsWith('https://');\n}","tryCatchPattern":"try {\n  const res = await fetch(url);\n  if (res.status === 403) {\n    // signature expired — re-resolve the signed URL and retry once\n    url = await resolveSignedUrl(page, id);\n    return download(url, id);\n  }\n  if (!res.ok) throw new Error(`CDN returned HTTP ${res.status} for ${id}`);\n} catch (e) {\n  console.error(`download failed for ${id}: ${e.message}`);\n}","preventionTips":["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)."],"tags":["http-error","cdn","signed-url","download","expired-url"],"backgroundTag":"signed-url-expired","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}