slopus/happy · error · Error

request-download returned no downloadUrl

Error message

request-download returned no downloadUrl

What it means

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.

Source

Thrown at packages/happy-cli/src/api/apiSession.ts:501

    /**
     * Download an encrypted attachment blob via the request-download flow:
     * POST /request-download → { downloadUrl } → GET downloadUrl. Local mode
     * downloadUrl points back at our server (Bearer required); S3 mode is a
     * presigned URL that does not accept extra headers.
     */
    async downloadAttachment(ref: string): Promise<Uint8Array> {
        const requestUrl = `${configuration.serverUrl}/v1/sessions/${this.sessionId}/attachments/request-download`;
        const requestRes = await axios.post(
            requestUrl,
            { ref },
            {
                headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' },
                timeout: 30000,
            },
        );
        const downloadUrl = requestRes.data?.downloadUrl;
        if (typeof downloadUrl !== 'string') {
            throw new Error('request-download returned no downloadUrl');
        }

        const isServerUrl = downloadUrl.startsWith(configuration.serverUrl);
        const headers: Record<string, string> = {};
        if (isServerUrl) {
            headers['Authorization'] = `Bearer ${this.token}`;
        }
        const response = await axios.get(downloadUrl, {
            headers,
            responseType: 'arraybuffer',
            timeout: 60000,
            maxRedirects: 5,
            maxContentLength: 10 * 1024 * 1024,
        });
        return new Uint8Array(response.data);
    }

    /**

View on GitHub (pinned to b824cd0a46)

Solutions

  1. Verify the attachment ID and that it belongs to the current session/account
  2. Re-authenticate — an expired/invalid token often yields an empty payload
  3. Align CLI and server versions so the request-download response shape matches
Defensive patterns

Strategy: validation

Validate before calling

const res = await requestDownload(attachmentId);
if (typeof res?.data?.downloadUrl !== 'string') {
  throw new Error(`no downloadUrl for attachment ${attachmentId}`);
}

Type guard

function hasDownloadUrl(d: unknown): d is { downloadUrl: string } {
  return typeof d === 'object' && d !== null
    && typeof (d as any).downloadUrl === 'string';
}

Try / catch

try {
  await downloadAttachment(attachmentId);
} catch (e) {
  if (e.message.includes('no downloadUrl')) {
    // attachment missing/expired or token invalid — re-auth and verify ID
  } throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: Downloading an attachment after it was deleted/expired; auth token from a different account; pointing at a server version that renamed the response field.

Related errors


AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31). Data as JSON: /api/errors/0ce3c020ca25abc1. Report an issue: GitHub.