{"record":{"id":"93dd3ed8c7bfa167","repo":"bytedance/deer-flow","slug":"upload-failed","errorCode":null,"errorMessage":"Upload failed","messagePattern":"Upload failed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/core/uploads/api.ts","lineNumber":70,"sourceCode":"  threadId: string,\n  files: File[],\n): Promise<UploadResponse> {\n  const formData = new FormData();\n\n  files.forEach((file) => {\n    formData.append(\"files\", file);\n  });\n\n  const response = await fetch(\n    `${getBackendBaseURL()}/api/threads/${threadId}/uploads`,\n    {\n      method: \"POST\",\n      body: formData,\n    },\n  );\n\n  if (!response.ok) {\n    throw new Error(await readErrorDetail(response, \"Upload failed\"));\n  }\n\n  return response.json();\n}\n\n/**\n * Load the upload limits enforced by the gateway for a thread\n */\nexport async function getUploadLimits(threadId: string): Promise<UploadLimits> {\n  const response = await fetch(\n    `${getBackendBaseURL()}/api/threads/${threadId}/uploads/limits`,\n  );\n\n  if (!response.ok) {\n    throw new Error(\n      await readErrorDetail(response, \"Failed to load upload limits\"),\n    );\n  }","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/frontend/src/core/uploads/api.ts#L52-L88","documentation":"uploadFiles POSTs multipart form data ('files' entries) to /api/threads/{threadId}/uploads and throws on any non-ok response. readErrorDetail prefers the gateway's error detail (e.g. file too large, disallowed type) and falls back to 'Upload failed' when the body has none.","triggerScenarios":"Uploading a file exceeding the gateway's size limit, a disallowed MIME type/extension, uploading to a deleted/nonexistent thread id (404), expired session (401), or gateway storage backend failure (5xx).","commonSituations":"Users attaching large videos/archives past the configured limit, uploading immediately after session expiry, thread deleted in another tab while the upload dialog was open.","solutions":["Fetch and enforce the limits first via getUploadLimits(threadId) — validate size/type client-side before the POST.","Match the error detail the gateway returned (it names the actual limit or reason) in the Network tab.","401: re-authenticate; 404: verify the thread still exists before retrying.","5xx: check gateway logs for the uploads handler and storage mount health."],"exampleFix":"// before: fire and hope\nconst res = await uploadFiles(threadId, files);\n\n// after: pre-validate against gateway limits\nconst limits = await getUploadLimits(threadId);\nconst oversized = files.filter((f) => f.size > limits.max_file_size);\nif (oversized.length) { throw new Error(`File exceeds ${limits.max_file_size} bytes: ${oversized[0].name}`); }\nconst res = await uploadFiles(threadId, files);","handlingStrategy":"validation","validationCode":"const limits = await getUploadLimits(threadId);\nconst tooBig = files.filter((f) => f.size > limits.max_file_size);\nconst tooMany = files.length > limits.max_files;\nif (tooBig.length || tooMany) { /* block and explain before POST */ }","typeGuard":"function isWithinLimits(files: File[], limits: UploadLimits): boolean {\n  return files.length <= limits.max_files && files.every((f) => f.size <= limits.max_file_size);\n}","tryCatchPattern":"try { const res = await uploadFiles(threadId, files); } catch (e) { toast(e.message || 'Upload failed'); keepFilesSelectedForRetry(); }","preventionTips":["Always fetch limits once per thread and validate client-side first.","Show per-file size/type feedback at selection time.","Preserve the user's selection on failure so a retry is one click."],"tags":["upload","attachments","api","limits","frontend"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}