{"record":{"id":"0ce3c020ca25abc1","repo":"slopus/happy","slug":"request-download-returned-no-downloadurl","errorCode":null,"errorMessage":"request-download returned no downloadUrl","messagePattern":"request-download returned no downloadUrl","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/happy-cli/src/api/apiSession.ts","lineNumber":501,"sourceCode":"    /**\n     * Download an encrypted attachment blob via the request-download flow:\n     * POST /request-download → { downloadUrl } → GET downloadUrl. Local mode\n     * downloadUrl points back at our server (Bearer required); S3 mode is a\n     * presigned URL that does not accept extra headers.\n     */\n    async downloadAttachment(ref: string): Promise<Uint8Array> {\n        const requestUrl = `${configuration.serverUrl}/v1/sessions/${this.sessionId}/attachments/request-download`;\n        const requestRes = await axios.post(\n            requestUrl,\n            { ref },\n            {\n                headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },\n                timeout: 30000,\n            },\n        );\n        const downloadUrl = requestRes.data?.downloadUrl;\n        if (typeof downloadUrl !== 'string') {\n            throw new Error('request-download returned no downloadUrl');\n        }\n\n        const isServerUrl = downloadUrl.startsWith(configuration.serverUrl);\n        const headers: Record<string, string> = {};\n        if (isServerUrl) {\n            headers['Authorization'] = `Bearer ${this.token}`;\n        }\n        const response = await axios.get(downloadUrl, {\n            headers,\n            responseType: 'arraybuffer',\n            timeout: 60000,\n            maxRedirects: 5,\n            maxContentLength: 10 * 1024 * 1024,\n        });\n        return new Uint8Array(response.data);\n    }\n\n    /**","sourceCodeStart":483,"sourceCodeEnd":519,"githubUrl":"https://github.com/slopus/happy/blob/b824cd0a4681d41af631a8e422a813873e4455b0/packages/happy-cli/src/api/apiSession.ts#L483-L519","documentation":"Thrown by downloadAttachment when the request-download response does not contain a string downloadUrl. The client requires a pre-signed/pre-authenticated URL to fetch the attachment; its absence means the server refused or failed to generate one.","triggerScenarios":"Attachment ID does not exist or belongs to another session; token lacks permission; server returns { data: null } or an error envelope instead of { downloadUrl }; API shape change between client and server versions.","commonSituations":"Downloading an attachment after it was deleted/expired; auth token from a different account; pointing at a server version that renamed the response field.","solutions":["Verify the attachment ID and that it belongs to the current session/account","Re-authenticate — an expired/invalid token often yields an empty payload","Align CLI and server versions so the request-download response shape matches"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const res = await requestDownload(attachmentId);\nif (typeof res?.data?.downloadUrl !== 'string') {\n  throw new Error(`no downloadUrl for attachment ${attachmentId}`);\n}","typeGuard":"function hasDownloadUrl(d: unknown): d is { downloadUrl: string } {\n  return typeof d === 'object' && d !== null\n    && typeof (d as any).downloadUrl === 'string';\n}","tryCatchPattern":"try {\n  await downloadAttachment(attachmentId);\n} catch (e) {\n  if (e.message.includes('no downloadUrl')) {\n    // attachment missing/expired or token invalid — re-auth and verify ID\n  } throw e;\n}","preventionTips":["Verify the attachment ID exists before downloading","Refresh tokens before long download sessions","Keep client/server API shapes aligned"],"tags":["api-contract","download","response-validation","attachments"],"backgroundTag":"missing-response-field","analyzedSha":"b824cd0a4681d41af631a8e422a813873e4455b0","analyzedAt":"2026-08-31T23:12:36.205Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}