{"record":{"id":"4512d34f64bb717f","repo":"twentyhq/twenty","slug":"download-failed-with-status-response-status","errorCode":null,"errorMessage":"download failed with status ${response.status}","messagePattern":"download failed with status (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/twenty-apps/public/call-recorder/src/logic-functions/flows/import-call-recording-media.util.ts","lineNumber":207,"sourceCode":"  const response = await fetch(url, {\n    signal: AbortSignal.timeout(MEDIA_DOWNLOAD_TIMEOUT_MS),\n  });\n  const contentLengthBytes = parseContentLengthBytes(\n    response.headers.get('content-length'),\n  );\n\n  console.log(\n    `[call-recorder] media-import phase=artifact-download-response callRecordingId=${callRecordingId} fileName=${fileName} responseStatus=${response.status} contentLengthBytes=${contentLengthBytes ?? 'unknown'} ${formatMemoryUsageForLog()}`,\n  );\n\n  if (!response.ok) {\n    await cancelMediaDownloadBody({\n      callRecordingId,\n      fileName,\n      body: response.body,\n    });\n\n    throw new Error(`download failed with status ${response.status}`);\n  }\n\n  if (isUndefined(contentLengthBytes)) {\n    await cancelMediaDownloadBody({\n      callRecordingId,\n      fileName,\n      body: response.body,\n    });\n\n    throw new Error('download response is missing content-length');\n  }\n\n  if (contentLengthBytes > maxMediaFileSizeBytes) {\n    await cancelMediaDownloadBody({\n      callRecordingId,\n      fileName,\n      body: response.body,\n    });","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/twentyhq/twenty/blob/1f5dd2bbd2a8da3419c8cfd52dd545c0024df1a6/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/import-call-recording-media.util.ts#L189-L225","documentation":"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.).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (!response.ok) {\n  await cancelMediaDownloadBody({ callRecordingId, fileName, body: response.body });\n  throw new Error(`download failed with status ${response.status}`);\n}\n\n// after — retry once with a freshly fetched media URL on 404/410\nif (!response.ok) {\n  await cancelMediaDownloadBody({ callRecordingId, fileName, body: response.body });\n  if (response.status === 404 || response.status === 410) {\n    throw new Error(\n      `download failed with status ${response.status} (media URL likely expired; re-fetch recording ${externalRecordingId})`,\n    );\n  }\n  throw new Error(`download failed with status ${response.status}`);\n}","handlingStrategy":"retry","validationCode":"// Fetch a fresh recording right before download so the media URL is not stale.\nconst recording = await getRecallRecording({ externalRecordingId });\nif (!recording.ok) {\n  throw new Error(`Cannot download media: Recall recording fetch failed (${recording.errorMessage})`);\n}\nconst mediaUrls = extractRecallMediaUrls(recording.recording);\nif (mediaUrls.length === 0) {\n  throw new Error('Recall recording has no media artifacts to download');\n}","typeGuard":null,"tryCatchPattern":"async function downloadWithRetry(url: string, attempts: number): Promise<Response> {\n  let lastStatus = 0;\n  for (let i = 0; i < attempts; i++) {\n    const res = await fetch(url, { signal: AbortSignal.timeout(MEDIA_DOWNLOAD_TIMEOUT_MS) });\n    if (res.ok) return res;\n    lastStatus = res.status;\n    await res.body?.cancel().catch(() => {});\n    // Retry only transient statuses; do not retry 404/410.\n    if (res.status < 500 && res.status !== 429) break;\n    await new Promise((r) => setTimeout(r, 2 ** i * 500));\n  }\n  throw new Error(`download failed with status ${lastStatus}`);\n}","preventionTips":["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."],"tags":["network","recall","call-recorder","media-download","http"],"backgroundTag":null,"analyzedSha":"1f5dd2bbd2a8da3419c8cfd52dd545c0024df1a6","analyzedAt":"2026-08-12T15:37:27.593Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}