{"record":{"id":"038c16ecf5cad900","repo":"ComposioHQ/composio","slug":"failed-to-download-file-response-statustext","errorCode":null,"errorMessage":"Failed to download file: ${response.statusText}","messagePattern":"Failed to download file: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ts/packages/core/src/utils/fileUtils.node.ts","lineNumber":365,"sourceCode":"  mimeType: string;\n  /**\n   * Absolute path to the directory to save the file into. When omitted, falls\n   * back to `<home>/.composio/files` (the legacy default).\n   */\n  fileDownloadDir?: string;\n  signal?: AbortSignal;\n  /**\n   * Maximum number of bytes to read from the response before rejecting.\n   * Defaults to {@link MAX_URL_UPLOAD_SIZE_BYTES} (100 MiB).\n   */\n  maxDownloadBytes?: number;\n}): Promise<FileDownloadData> => {\n  // SSRF guard: `s3Url` is a field of the tool-execution response, so it is no\n  // more trusted than a user-supplied URL — and the bytes it returns are\n  // written to disk. See ssrfGuard.node.ts.\n  const response = await ssrfSafeFetch(s3Url, { signal });\n  if (!response.ok) {\n    throw new Error(`Failed to download file: ${response.statusText}`);\n  }\n  // The response is attacker-influencable and streamed to disk, so the body is\n  // read through the shared size guard rather than buffered wholesale.\n  const data = await readResponseBodyWithLimit(response, maxDownloadBytes);\n\n  const extension = getExtensionFromMimeType(mimeType);\n  const fileName = generateTimestampedFilename(extension, `${toolSlug}_`);\n  const filePath = saveFile(fileName, data, {\n    isTempFile: fileDownloadDir === undefined,\n    outputDir: fileDownloadDir,\n  });\n  return {\n    name: fileName,\n    mimeType: mimeType,\n    s3Url: s3Url,\n\n    /**\n     * @todo: fix in follow-up PR.","sourceCodeStart":347,"sourceCodeEnd":383,"githubUrl":"https://github.com/ComposioHQ/composio/blob/64b1b85502b1beeb2379e6c9e8bf1104504fa637/ts/packages/core/src/utils/fileUtils.node.ts#L347-L383","documentation":"downloadFileFromS3 fetches the s3Url from a tool-execution response via ssrfSafeFetch; a non-OK status throws with the status text. The body would be streamed to disk, so it is size-limited but the error path occurs before that.","triggerScenarios":"A tool execution returns an s3Url that is expired (403 S3 signature), deleted (404), or the host returns 5xx during file download.","commonSituations":"Downloading tool output files long after execution finished, cross-region S3 issues, or backend-generated URLs with short TTLs.","solutions":["Re-execute the tool or re-request the download URL to get a fresh signed link.","Retry with backoff for transient 5xx.","Inspect the s3Url manually with curl to see the exact status."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await downloadFileFromS3(url, {...}); } catch (e) {\n  if ((e as Error).message.startsWith('Failed to download file:')) { /* refresh url / re-execute tool */ }\n}","preventionTips":["Download result files promptly after execution.","Retry transient S3 5xx with backoff."],"tags":["s3","download","network","typescript"],"backgroundTag":"file-download-failed","analyzedSha":"64b1b85502b1beeb2379e6c9e8bf1104504fa637","analyzedAt":"2026-08-28T15:39:33.623Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}