twentyhq/twenty · error · Error
download failed with status ${response.status}
Error message
download failed with status ${response.status} What it means
During media import, the call-recorder downloads a recording artifact (video/audio) from Recall.ai over HTTP. After streaming-log of the response, it checks `response.ok`; if Recall returned a non-2xx status the code cancels the response body (to avoid leaking the connection) and throws this error. The status code is interpolated, so the message tells you exactly what Recall returned (404, 403, 502, etc.).
Source
Thrown at packages/twenty-apps/public/call-recorder/src/logic-functions/flows/import-call-recording-media.util.ts:207
const response = await fetch(url, {
signal: AbortSignal.timeout(MEDIA_DOWNLOAD_TIMEOUT_MS),
});
const contentLengthBytes = parseContentLengthBytes(
response.headers.get('content-length'),
);
console.log(
`[call-recorder] media-import phase=artifact-download-response callRecordingId=${callRecordingId} fileName=${fileName} responseStatus=${response.status} contentLengthBytes=${contentLengthBytes ?? 'unknown'} ${formatMemoryUsageForLog()}`,
);
if (!response.ok) {
await cancelMediaDownloadBody({
callRecordingId,
fileName,
body: response.body,
});
throw new Error(`download failed with status ${response.status}`);
}
if (isUndefined(contentLengthBytes)) {
await cancelMediaDownloadBody({
callRecordingId,
fileName,
body: response.body,
});
throw new Error('download response is missing content-length');
}
if (contentLengthBytes > maxMediaFileSizeBytes) {
await cancelMediaDownloadBody({
callRecordingId,
fileName,
body: response.body,
});View on GitHub (pinned to 1f5dd2bbd2)
Solutions
- Read the interpolated status: 404/410 → the artifact URL is gone, re-fetch the recording from Recall to get a fresh media URL before downloading; 403 → check the Recall API key / token scope; 5xx → retry after backoff.
- Re-fetch the Recall recording (`getRecallRecording`) immediately before download to obtain a fresh presigned media URL rather than reusing a cached one.
- Confirm the call recording's `externalRecordingId` still maps to a Recall recording that has finished processing.
- If the artifact is permanently unavailable, mark the call recording failed with the appropriate failure reason instead of retrying indefinitely.
Example fix
// before
if (!response.ok) {
await cancelMediaDownloadBody({ callRecordingId, fileName, body: response.body });
throw new Error(`download failed with status ${response.status}`);
}
// after — retry once with a freshly fetched media URL on 404/410
if (!response.ok) {
await cancelMediaDownloadBody({ callRecordingId, fileName, body: response.body });
if (response.status === 404 || response.status === 410) {
throw new Error(
`download failed with status ${response.status} (media URL likely expired; re-fetch recording ${externalRecordingId})`,
);
}
throw new Error(`download failed with status ${response.status}`);
} Defensive patterns
Strategy: retry
Validate before calling
// Fetch a fresh recording right before download so the media URL is not stale.
const recording = await getRecallRecording({ externalRecordingId });
if (!recording.ok) {
throw new Error(`Cannot download media: Recall recording fetch failed (${recording.errorMessage})`);
}
const mediaUrls = extractRecallMediaUrls(recording.recording);
if (mediaUrls.length === 0) {
throw new Error('Recall recording has no media artifacts to download');
} Try / catch
async function downloadWithRetry(url: string, attempts: number): Promise<Response> {
let lastStatus = 0;
for (let i = 0; i < attempts; i++) {
const res = await fetch(url, { signal: AbortSignal.timeout(MEDIA_DOWNLOAD_TIMEOUT_MS) });
if (res.ok) return res;
lastStatus = res.status;
await res.body?.cancel().catch(() => {});
// Retry only transient statuses; do not retry 404/410.
if (res.status < 500 && res.status !== 429) break;
await new Promise((r) => setTimeout(r, 2 ** i * 500));
}
throw new Error(`download failed with status ${lastStatus}`);
} Prevention
- Always re-fetch the Recall recording immediately before download to get a fresh presigned URL.
- Distinguish 4xx (permanent) from 5xx/429 (retryable) and only retry the latter.
- Stream-log the status, content-length, and callRecordingId for every download attempt.
- Set a download timeout so a hanging connection fails fast rather than blocking the flow.
When it happens
Trigger: Recall.ai returns 404 (the media URL expired or the recording artifact was deleted), 403 (auth/token problem on the media host), 410 (artifact retired), or 5xx (Recall upstream outage). The download URL comes from `getRecallRecording` / `extractRecallMediaUrls`, so a stale or presigned-expired URL is the most common trigger.
Common situations: Webhooks processed long after the recording completed (presigned media URLs expired), a Recall migration that changed media host paths, regional Recall outages, or an expired/rotated Recall API credential producing 403s on the media endpoint.
Related errors
- download response is missing content-length
- download returned no body
- Server error (${response.status})
- failed to request artifact import for call recording ${callR
- RECALL_WEBHOOK_SECRET server variable is not set. A server a
AI-assisted analysis of twentyhq/twenty@1f5dd2bbd2 (2026-08-12).
Data as JSON: /api/errors/4512d34f64bb717f.
Report an issue: GitHub.