{"record":{"id":"4eb17e799507ece5","repo":"Budibase/budibase","slug":"failed-to-download-asset-response-statustext","errorCode":null,"errorMessage":"Failed to download asset: ${response.statusText}","messagePattern":"Failed to download asset: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/automations/steps/ai/extract.ts","lineNumber":91,"sourceCode":"    \"The data array must contain at most 1 object.\",\n    \"Do not include markdown, explanations, or extra keys.\",\n    'If no matching data is found, return {\"data\": []}.',\n  ].join(\"\\n\\n\")\n}\n\nconst downloadAssetsForExtract: Experimental_DownloadFunction =\n  async requests =>\n    Promise.all(\n      requests.map(async ({ url, isUrlSupportedByModel }) => {\n        if (url.protocol === \"data:\") {\n          return null\n        }\n        if (isUrlSupportedByModel) {\n          return null\n        }\n        const response = await fetch(url)\n        if (!response.ok) {\n          throw new Error(`Failed to download asset: ${response.statusText}`)\n        }\n        return {\n          data: new Uint8Array(await response.arrayBuffer()),\n          mediaType: response.headers.get(\"content-type\") ?? undefined,\n        }\n      })\n    )\n\nfunction buildExtractModelMessages(input: ExtractInput): ModelMessage[] {\n  const prompt = buildExtractPrompt()\n  const userContent: UserContent =\n    input.kind === \"image\"\n      ? [\n          {\n            type: \"image\",\n            image: new URL(input.value),\n          },\n          {","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/automations/steps/ai/extract.ts#L73-L109","documentation":"downloadAssetsForExtract fetches model-unsupported URLs manually (when the LLM cannot download the asset itself) using plain fetch. Any non-OK HTTP status causes it to throw with the response's statusText, aborting the extract step's asset preparation.","triggerScenarios":"fetch(url) in downloadAssetsForExtract returns response.ok === false — e.g. 404 (asset moved/deleted), 403 (private/permission-denied URL), 500 from the asset server — for a URL the model cannot ingest directly.","commonSituations":"Attachment/upload URLs that expired (signed URLs past expiry); assets behind authentication; public URLs that later 404; CDN or storage (S3/MinIO) misconfiguration returning errors.","solutions":["Verify the asset URL is publicly reachable (curl it) and returns 200","Re-upload or regenerate the asset / signed URL if it expired or was deleted","Ensure the URL requires no auth headers that plain fetch does not send","Check the asset server/CDN logs for the failing status"],"exampleFix":"// before\nconst response = await fetch(expiredSignedUrl) // 403 Forbidden\n// after\nconst freshUrl = await regenerateSignedUrl(assetId)\nconst response = await fetch(freshUrl) // 200 OK","handlingStrategy":"retry","validationCode":"const head = await fetch(url, { method: \"HEAD\" })\nif (!head.ok) throw new Error(`Asset URL unreachable: ${head.status}`)","typeGuard":null,"tryCatchPattern":"try {\n  await downloadAssetsForExtract(url, isUrlSupportedByModel)\n} catch (err) {\n  if (err.message.startsWith(\"Failed to download asset:\")) {\n    // retry with backoff or refresh the asset URL\n  }\n  throw err\n}","preventionTips":["Use stable, publicly reachable URLs for assets","Regenerate signed URLs before long-running automations","Check response.ok yourself and retry transient 5xx with backoff"],"tags":["http","fetch","network","ai","asset-download"],"backgroundTag":"http-request-failed","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}