{"record":{"id":"43d1f396b307ff54","repo":"paperclipai/paperclip","slug":"typeof-result-error-string-result-error-project-tool-failed","errorCode":null,"errorMessage":"typeof result.error === \"string\" ? result.error : `Project tool failed (${response.status})`","messagePattern":"typeof result\\.error === \"string\" \\? result\\.error : `Project tool failed \\((.+?)\\)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"server/src/services/project-tools.ts","lineNumber":50,"sourceCode":"    path = `/companies/${input.companyId}/issues`;\n    body = createIssueSchema.parse({\n      title: args.title, description: args.description, priority: args.priority,\n      projectId: args.projectId, initialPlan: args.initialPlan,\n      assigneeAgentId: args.assigneeActorId ?? input.agentId,\n      parentId: input.conversation ? null : input.issueId,\n      status: Array.isArray(args.blockedByTaskIds) && args.blockedByTaskIds.length ? \"blocked\" : \"todo\",\n      blockedByIssueIds: args.blockedByTaskIds,\n      idempotencyKey: `chat-handoff:${input.issueId}:${key}`,\n    });\n  } else throw badRequest(\"Unknown project tool\");\n  const response = await fetch(`${input.apiUrl.replace(/\\/+$/, \"\").replace(/\\/api$/, \"\")}/api${path}`, {\n    method: body ? \"POST\" : \"GET\",\n    headers: { Authorization: `Bearer ${input.token}`, \"Content-Type\": \"application/json\" },\n    ...(body ? { body: JSON.stringify(body) } : {}),\n    signal: AbortSignal.timeout(60_000),\n  });\n  const result = await response.json();\n  if (!response.ok) throw new Error(typeof result.error === \"string\" ? result.error : `Project tool failed (${response.status})`);\n  return input.name === \"list_projects\" ? { projects: result } : result;\n}\n","sourceCodeStart":32,"sourceCodeEnd":53,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/project-tools.ts#L32-L53","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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>"],"exampleFix":"// before: raw call surfaces generic failure\nawait callProjectTool({ name: \"create_task\", arguments: { projectId: \"p_old\" }, ... });\n// throws: Project tool failed (404)\n// after: validate references and handle API errors explicitly\nconst project = await callProjectTool({ name: \"list_projects\", arguments: {}, ... });\nif (!project.projects.some((p) => p.id === args.projectId)) throw new Error(`Unknown projectId ${args.projectId}`);\ntry {\n  return await callProjectTool({ name: \"create_task\", arguments: args, ... });\n} catch (e) {\n  if (String(e.message).includes(\"401\")) { await refreshToken(); return retry(); }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":"// pre-validate arguments against the tool schema before calling the API\nconst parsed = tool.inputSchema.safeParse(args);\nif (!parsed.success) throw new Error(`Invalid ${tool.name} arguments: ${parsed.error.message}`);\nif (tool.name === \"create_task\" && args.projectId) {\n  // verify the project exists/ is accessible first via list_projects\n}","typeGuard":"function isProjectToolHttpError(e: unknown): e is Error & { status?: number } {\n  return e instanceof Error && /^Project tool failed \\(\\d{3}\\)$/.test(e.message) ||\n    (e instanceof Error && typeof (e as { status?: number }).status === \"number\");\n}","tryCatchPattern":"try {\n  return await callProjectTool(input);\n} catch (e) {\n  const msg = e instanceof Error ? e.message : String(e);\n  const status = Number(msg.match(/\\((\\d{3})\\)$/)?.[1] ?? 0);\n  if (status === 401 || status === 403) throw new Error(`Auth failure for project tool ${input.name}: refresh token / check company access`);\n  if (status >= 500) return withBackoff(() => callProjectTool(input)); // transient, idempotencyKey makes retry safe\n  throw e; // 4xx: surface the API's error message to the agent\n}","preventionTips":["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)"],"tags":["http","api","project-tools","error-handling"],"backgroundTag":"http-error-response","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"}