{"record":{"id":"b65bd5de2b81191e","repo":"paperclipai/paperclip","slug":"cannot-build-api-path-with-an-empty-path-segment","errorCode":null,"errorMessage":"Cannot build API path with an empty path segment.","messagePattern":"Cannot build API path with an empty path segment\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/src/commands/client/common.ts","lineNumber":128,"sourceCode":"\nexport function resolveApiBase(options: Pick<BaseClientOptions, \"apiBase\" | \"config\">, profile: ClientContextProfile = {}): string {\n  return normalizeApiBase(\n    options.apiBase?.trim() ||\n    process.env.PAPERCLIP_API_URL?.trim() ||\n    profile.apiBase ||\n    inferApiBaseFromConfig(options.config),\n  );\n}\n\nexport function normalizeApiBase(apiBase: string): string {\n  return apiBase.trim().replace(/\\/+$/, \"\");\n}\n\nexport function apiPath(strings: TemplateStringsArray, ...values: Array<string | number | boolean | null | undefined>): string {\n  let path = strings[0] ?? \"\";\n  values.forEach((value, index) => {\n    if (value === null || value === undefined || String(value).trim() === \"\") {\n      throw new Error(\"Cannot build API path with an empty path segment.\");\n    }\n    path += `${encodeURIComponent(String(value))}${strings[index + 1] ?? \"\"}`;\n  });\n  return path;\n}\n\nexport function inferContentTypeFromPath(filePath: string): string | undefined {\n  const ext = filePath.split(/[\\\\/]/).pop()?.split(\".\").pop()?.toLowerCase();\n  if (!ext) return undefined;\n  // These MIME strings are matched against the server's issue-attachment\n  // allowlist (server/src/attachment-types.ts DEFAULT_ALLOWED_TYPES) by EXACT\n  // string, so text types must carry no \"; charset=...\" parameter or the upload\n  // is rejected with \"422 Unsupported attachment content type\". Keep this set in\n  // sync with that allowlist (plus svg/avif, accepted by the asset routes).\n  return {\n    avif: \"image/avif\",\n    csv: \"text/csv\",\n    gif: \"image/gif\",","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/paperclipai/paperclip/blob/120ae5428fa29bee300bcf806491cd4d965fbb7c/cli/src/commands/client/common.ts#L110-L146","documentation":"HTTP 404 with body {\"error\":\"User secret value not found\"} from DELETE /api/companies/:companyId/me/user-secrets/:secretId (secrets.ts:856). svc.removeCurrentUserSecretValue(companyId, ownerUserId, secretId) returned null: no value row exists for this (company, current user, secretId) triple at removal time - already deleted, owned by a different user, or a bad ID. Route requires board auth plus company access, and the value must belong to the calling user.","triggerScenarios":"Double-delete of the same personal secret value; deleting a value the current user never had (e.g. another user's secretId); retrying a delete that already succeeded; deleting after switching board user context.","commonSituations":"Settings pages with double-submitted delete actions; test scripts cycling multiple user accounts against a shared list of secret IDs; deprovisioning helpers that run after the value was already cleared.","solutions":["Treat 404 on DELETE as already-removed (idempotent success) when removal is the goal.","Drive deletions from the current user's own secret list, refreshed immediately before the call.","Ensure the authenticated board user is the value's owner before scripting deletes.","Disable delete controls after first click and re-sync the list from the response."],"exampleFix":"// before\nconst res = await api.delete(`/api/companies/${companyId}/me/user-secrets/${secretId}`);\nif (!res.ok) throw new Error('delete failed');\n\n// after\nconst res = await api.delete(`/api/companies/${companyId}/me/user-secrets/${secretId}`);\nif (res.status === 404) {\n  logger.info(`secret ${secretId} already absent for this user; done`);\n} else if (!res.ok) {\n  throw new Error(`delete failed: ${res.status}`);\n}","handlingStrategy":"fallback","validationCode":"async function deleteMySecretValueSafe(api: ApiClient, companyId: string, secretId: string) {\n  const res = await api.fetch(`/api/companies/${companyId}/me/user-secrets/${secretId}`, {\n    method: 'DELETE',\n  });\n  if (res.status === 404) return { deleted: true, alreadyGone: true }; // idempotent\n  if (!res.ok) throw new Error(`delete failed: ${res.status}`);\n  return { deleted: true, alreadyGone: false };\n}","typeGuard":"function isApiErrorBody(body: unknown): body is { error: string } {\n  return typeof body === 'object' && body !== null &&\n    typeof (body as Record<string, unknown>).error === 'string';\n}\nconst isValueNotFound = (b: unknown): boolean =>\n  isApiErrorBody(b) && b.error === 'User secret value not found';","tryCatchPattern":"try {\n  await api.delete(`/api/companies/${companyId}/me/user-secrets/${secretId}`);\n} catch (err) {\n  if (err instanceof ApiError && err.status === 404 && isValueNotFound(err.body)) {\n    return; // already gone or never owned by this user - success for removal intent\n  }\n  throw err;\n}","preventionTips":["Treat 404 on personal secret DELETE as success in cleanup flows.","Source deletion targets from the current user's own secret list, fetched just before.","Ensure the authenticated board user matches the value's owner in shared tooling.","Re-sync the secret list from delete responses to keep UI state truthful."],"tags":["http-404","express","secrets","user-secret-values","delete","idempotency","owner-scoping","paperclip"],"backgroundTag":"http-404-resource-not-found","analyzedSha":"120ae5428fa29bee300bcf806491cd4d965fbb7c","analyzedAt":"2026-08-18T22:49:45.177Z","contentChangedAt":"2026-08-18T22:49:45.177Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}