paperclipai/paperclip · error · Error
Upload did not return a file URL
Error message
Upload did not return a file URL
What it means
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.
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.
Example fix
// before (host integration)
const res = await upload(file);
return { id: attachmentId }; // missing contentPath
// after
const res = await upload(file);
if (!res?.url) throw new Error(`upload failed: ${res?.error ?? "no url"}`);
return { id: attachmentId, contentPath: res.url }; Defensive patterns
Strategy: validation
Validate before calling
const attachment = await onAttachImage(file);
if (!attachment?.contentPath) { showAttachError("upload returned no URL"); return; } Type guard
function hasContentPath(a: unknown): a is { contentPath: string } & Record<string, unknown> {
return typeof a === "object" && a !== null && typeof (a as any).contentPath === "string" && (a as any).contentPath.length > 0;
} Try / catch
try {
const url = await attachImage(file);
addComposerAttachment(attachmentId, url);
} catch (e) {
setComposerError(attachmentId, e.message); // show inline failure, keep draft intact
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Select a company to upload images
- Task details are still loading. Please try again.
- ACPX sidecar runtime context must be pre-materialized
- [adapter-ui-loader] Failed to load UI parser for
- Add an objective after /goal to start a goal.
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/8cb733fb375fbf4f.
Report an issue: GitHub.
Appendix: source
Thrown at ui/src/components/IssueChatThread.tsx:5114
)
)
return undefined;
const safeName = file.name.replace(/[[\]]/g, "\\$&");
const markdown = ``;
if (insertInline)
changeBody((prev) => (prev ? `${prev}\n\n${markdown}` : markdown));
setComposerAttachments((prev) =>
prev.map((item) =>
item.id === attachmentId
? { ...item, status: "attached", contentPath: url }
: item,
),
);
return url;
} else if (onAttachImage) {
const attachment = await onAttachImage(file);
if (!attachment?.contentPath)
throw new Error("Upload did not return a file URL");
if (
!composerAttachmentsRef.current.some(
(item) => item.id === attachmentId,
)
)
return undefined;
if (inline && insertInline) {
const markdown = `![${file.name.replace(/[[\]]/g, "\\$&")}](${attachment.contentPath})`;
changeBody((prev) => (prev ? `${prev}\n\n${markdown}` : markdown));
}
setComposerAttachments((prev) =>
prev.map((item) =>
item.id === attachmentId
? {
...item,
status: "attached",
attachmentId: attachment.id,
contentPath: attachment?.contentPath,View on GitHub (pinned to 3f1d897a7c)