{"record":{"id":"8cb733fb375fbf4f","repo":"paperclipai/paperclip","slug":"upload-did-not-return-a-file-url-issuechatthread","errorCode":null,"errorMessage":"Upload did not return a file URL","messagePattern":"Upload did not return a file URL","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ui/src/components/IssueChatThread.tsx","lineNumber":5114,"sourceCode":"          )\n        )\n          return undefined;\n        const safeName = file.name.replace(/[[\\]]/g, \"\\\\$&\");\n        const markdown = `![${safeName}](${url})`;\n        if (insertInline)\n          changeBody((prev) => (prev ? `${prev}\\n\\n${markdown}` : markdown));\n        setComposerAttachments((prev) =>\n          prev.map((item) =>\n            item.id === attachmentId\n              ? { ...item, status: \"attached\", contentPath: url }\n              : item,\n          ),\n        );\n        return url;\n      } else if (onAttachImage) {\n        const attachment = await onAttachImage(file);\n        if (!attachment?.contentPath)\n          throw new Error(\"Upload did not return a file URL\");\n        if (\n          !composerAttachmentsRef.current.some(\n            (item) => item.id === attachmentId,\n          )\n        )\n          return undefined;\n        if (inline && insertInline) {\n          const markdown = `![${file.name.replace(/[[\\]]/g, \"\\\\$&\")}](${attachment.contentPath})`;\n          changeBody((prev) => (prev ? `${prev}\\n\\n${markdown}` : markdown));\n        }\n        setComposerAttachments((prev) =>\n          prev.map((item) =>\n            item.id === attachmentId\n              ? {\n                  ...item,\n                  status: \"attached\",\n                  attachmentId: attachment.id,\n                  contentPath: attachment?.contentPath,","sourceCodeStart":5096,"sourceCodeEnd":5132,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/ui/src/components/IssueChatThread.tsx#L5096-L5132","documentation":"In the IssueChatThread composer's image attach flow, when the onAttachImage callback resolves, the code requires attachment.contentPath to be set as the file URL. If the callback returns an attachment without contentPath (or null), it throws \"Upload did not return a file URL\", aborting the composer attachment and showing the failure to the user.","triggerScenarios":"A custom onAttachImage handler (host-app provided upload path) resolves with { contentPath: undefined } or null — e.g. the host integration's upload endpoint returned no URL, or the callback swallowed an upload error and returned an incomplete attachment object.","commonSituations":"Embedding host app's upload service returns 200 with a body missing the URL field; plugin/host integration not updated to the attachment contract that requires contentPath; storage backend misconfigured so the upload silently produces no public URL; race where the attachment was discarded (attachmentId no longer in composerAttachmentsRef) — though that path returns undefined instead of throwing.","solutions":["Log the full attachment object returned by onAttachImage to see what the upload actually returned.","Fix the host-supplied onAttachImage implementation to return { contentPath: <url>, ... } on success and reject on failure instead of resolving an empty object.","Verify the upload endpoint's response parsing extracts the URL field correctly (field-name drift after API change).","Check storage configuration so the upload service returns a usable file URL."],"exampleFix":"// before (host integration)\nconst res = await upload(file);\nreturn { id: attachmentId }; // missing contentPath\n// after\nconst res = await upload(file);\nif (!res?.url) throw new Error(`upload failed: ${res?.error ?? \"no url\"}`);\nreturn { id: attachmentId, contentPath: res.url };","handlingStrategy":"validation","validationCode":"const attachment = await onAttachImage(file);\nif (!attachment?.contentPath) { showAttachError(\"upload returned no URL\"); return; }","typeGuard":"function hasContentPath(a: unknown): a is { contentPath: string } & Record<string, unknown> {\n  return typeof a === \"object\" && a !== null && typeof (a as any).contentPath === \"string\" && (a as any).contentPath.length > 0;\n}","tryCatchPattern":"try {\n  const url = await attachImage(file);\n  addComposerAttachment(attachmentId, url);\n} catch (e) {\n  setComposerError(attachmentId, e.message); // show inline failure, keep draft intact\n}","preventionTips":["Require every onAttachImage implementation to either resolve with a non-empty contentPath or reject — never resolve an incomplete object.","Add a contract test for host-supplied upload handlers asserting the attachment shape.","Verify the upload service returns a URL field and that response parsing uses the correct field name after API changes."],"tags":["ui","upload","contract"],"backgroundTag":"unexpected-response-shape","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}