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

  1. Inspect response.status — 401/403 means re-authenticate; 404 means wrong companyId/issueId; 413 means shrink the file or raise the server limit.
  2. Retry on transient 5xx with backoff (the upload is idempotent only if the server dedupes; otherwise confirm before retrying).
  3. 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

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


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/3c2d034a576181df. Report an issue: GitHub.