paperclipai/paperclip · error · Error
${uploadError}
Error message
${uploadError} What it means
Thrown by uploadIssueAttachmentFile() when the POST to /api/companies/:companyId/issues/:issueId/attachments returns a non-OK HTTP status. The message is produced by readUploadError(), which parses the JSON body and prefers body.error, then body.message, falling back to 'Attachment upload failed with HTTP <status>.' This is a pass-through of the server's own error.
Source
Thrown at packages/plugins/plugin-llm-wiki/src/ui/issue-attachments.ts:46
export async function uploadIssueAttachmentFile(input: {
companyId: string;
issueId: string;
file: File;
fetchImpl?: FetchLike;
}): Promise<unknown> {
const fetchImpl = input.fetchImpl ?? fetch;
const form = new FormData();
form.append("file", input.file);
const response = await fetchImpl(
`/api/companies/${encodeURIComponent(input.companyId)}/issues/${encodeURIComponent(input.issueId)}/attachments`,
{
method: "POST",
credentials: "include",
body: form,
},
);
if (!response.ok) {
throw new Error(await readUploadError(response));
}
return response.json();
}
View on GitHub (pinned to 67001ec6eb)
Solutions
- Inspect response.status — 401/403 means re-authenticate; 404 means wrong companyId/issueId; 413 means shrink the file or raise the server limit.
- Retry on transient 5xx with backoff (the upload is idempotent only if the server dedupes; otherwise confirm before retrying).
- Ensure credentials: "include" is sent alongside a valid session cookie for the Paperclip API origin.
Example fix
// before
await uploadIssueAttachmentFile({ companyId, issueId, file }); // bare throw
// after
try {
await uploadIssueAttachmentFile({ companyId, issueId, file });
} catch (err) {
if (err instanceof Error && /HTTP 401|HTTP 403/.test(err.message)) {
// re-authenticate and retry once
} else if (err instanceof Error && /HTTP 413/.test(err.message)) {
// file too large — compress or reject
} else {
throw err;
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-flight: ensure the target issue exists and session is valid
async function canUpload(fetchImpl: typeof fetch, companyId: string, issueId: string): Promise<boolean> {
const r = await fetchImpl(`/api/companies/${encodeURIComponent(companyId)}/issues/${encodeURIComponent(issueId)}`, { credentials: "include" });
return r.ok;
} Try / catch
try {
await uploadIssueAttachmentFile({ companyId, issueId, file });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (/HTTP 401|HTTP 403/.test(msg)) { /* re-auth */ }
else if (/HTTP 404/.test(msg)) { /* wrong ids */ }
else if (/HTTP 413/.test(msg)) { /* shrink file */ }
else throw err;
} Prevention
- Send credentials: "include" with a valid session cookie.
- Confirm companyId and issueId belong together before upload.
- Handle 5xx with bounded retry; surface the server's body.error/message.
When it happens
Trigger: Uploading to an issue that does not exist (404). Wrong company scope or missing credentials (401/403). File too large or wrong content-type (413/415). Server error (500). Network blip returning a 5xx.
Common situations: Cookie/session expired so credentials: "include" no longer authenticates. issueId from a different company. Reverse proxy rejecting multipart bodies above a limit. Temporary backend outage during upload.
Related errors
- Ingest operation did not return an issue id; the dropped fil
- Request failed: ${response.status}
- Request failed with status ${response.status}
- Export request returned no data
- API error ${response.status}: ${message}
AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12).
Data as JSON: /api/errors/3c2d034a576181df.
Report an issue: GitHub.