paperclipai/paperclip · error · Error
typeof result.error === "string" ? result.error : `Project…
Error message
typeof result.error === "string" ? result.error : `Project tool failed (${response.status})` What it means
callProjectTool() invokes Paperclip REST API endpoints (project/task tools) over authenticated HTTP. When the response status is not ok, it throws an Error whose message is the API's `error` string field if present, otherwise a generic `Project tool failed (<status>)`. This is a wrapper that surfaces server-side validation/permission/not-found failures from the underlying API call to the agent caller.
Solutions
- Read the thrown message: it carries the API's `error` field — fix the specific payload issue it names (missing/invalid projectId, title, validation constraints)
- Refresh the bearer token if the status was 401/403, and verify the agent key's company access
- Validate tool arguments against the tool input schema before calling callProjectTool
- For 5xx statuses, retry the call (it is idempotent-safe via idempotencyKey for create paths) after a short backoff
- Confirm apiUrl is the correct base (the code strips trailing slashes and a trailing /api) so the request reaches /api<path>
Example fix
// before: raw call surfaces generic failure
await callProjectTool({ name: "create_task", arguments: { projectId: "p_old" }, ... });
// throws: Project tool failed (404)
// after: validate references and handle API errors explicitly
const project = await callProjectTool({ name: "list_projects", arguments: {}, ... });
if (!project.projects.some((p) => p.id === args.projectId)) throw new Error(`Unknown projectId ${args.projectId}`);
try {
return await callProjectTool({ name: "create_task", arguments: args, ... });
} catch (e) {
if (String(e.message).includes("401")) { await refreshToken(); return retry(); }
throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-validate arguments against the tool schema before calling the API
const parsed = tool.inputSchema.safeParse(args);
if (!parsed.success) throw new Error(`Invalid ${tool.name} arguments: ${parsed.error.message}`);
if (tool.name === "create_task" && args.projectId) {
// verify the project exists/ is accessible first via list_projects
} Type guard
function isProjectToolHttpError(e: unknown): e is Error & { status?: number } {
return e instanceof Error && /^Project tool failed \(\d{3}\)$/.test(e.message) ||
(e instanceof Error && typeof (e as { status?: number }).status === "number");
} Try / catch
try {
return await callProjectTool(input);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
const status = Number(msg.match(/\((\d{3})\)$/)?.[1] ?? 0);
if (status === 401 || status === 403) throw new Error(`Auth failure for project tool ${input.name}: refresh token / check company access`);
if (status >= 500) return withBackoff(() => callProjectTool(input)); // transient, idempotencyKey makes retry safe
throw e; // 4xx: surface the API's error message to the agent
} Prevention
- Validate tool arguments against the zod input schema client-side before invoking the tool
- Resolve projectId via list_projects before create_task instead of trusting agent-supplied IDs
- Always pass a fresh idempotencyKey so 5xx retries cannot create duplicates
- Log the HTTP status alongside the message to distinguish auth (401/403), validation (400/422), and server (5xx) failures
- Verify apiUrl configuration once at startup (correct /api base, no trailing-slash issues)
When it happens
Trigger: Any non-2xx response from the fetch to `/api/companies/{companyId}/projects`, `/project-repositories`, or `/issues`: 400 for invalid create_project/create_task payloads (zod parse passed but server validation failed), 401 for an expired/invalid bearer token, 403 for insufficient company permissions, 404 for a missing projectId, 409 conflicts, 422 validation errors, or 5xx server errors.
Common situations: Agent passing a nonexistent projectId to create_task; token expiring mid-conversation (401); agent exceeding project name/slug validation rules (400/422); the API URL being misconfigured so the request hits a wrong route (404); transient 500/503 from the API server.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- Skill creation failed
- Discord failed (HTTP ` : ""} ` : ""})
- | null)?.error ?? `Request failed: `}
- OpenCode API request failed
- OpenCode API returned HTTP
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/43d1f396b307ff54.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/project-tools.ts:50
path = `/companies/${input.companyId}/issues`;
body = createIssueSchema.parse({
title: args.title, description: args.description, priority: args.priority,
projectId: args.projectId, initialPlan: args.initialPlan,
assigneeAgentId: args.assigneeActorId ?? input.agentId,
parentId: input.conversation ? null : input.issueId,
status: Array.isArray(args.blockedByTaskIds) && args.blockedByTaskIds.length ? "blocked" : "todo",
blockedByIssueIds: args.blockedByTaskIds,
idempotencyKey: `chat-handoff:${input.issueId}:${key}`,
});
} else throw badRequest("Unknown project tool");
const response = await fetch(`${input.apiUrl.replace(/\/+$/, "").replace(/\/api$/, "")}/api${path}`, {
method: body ? "POST" : "GET",
headers: { Authorization: `Bearer ${input.token}`, "Content-Type": "application/json" },
...(body ? { body: JSON.stringify(body) } : {}),
signal: AbortSignal.timeout(60_000),
});
const result = await response.json();
if (!response.ok) throw new Error(typeof result.error === "string" ? result.error : `Project tool failed (${response.status})`);
return input.name === "list_projects" ? { projects: result } : result;
}
View on GitHub (pinned to 3f1d897a7c)