paperclipai/paperclip · error

Cannot build API path with an empty path segment.

Error message

Cannot build API path with an empty path segment.

What it means

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.

Solutions

  1. Treat 404 on DELETE as already-removed (idempotent success) when removal is the goal.
  2. Drive deletions from the current user's own secret list, refreshed immediately before the call.
  3. Ensure the authenticated board user is the value's owner before scripting deletes.
  4. Disable delete controls after first click and re-sync the list from the response.

Example fix

// before
const res = await api.delete(`/api/companies/${companyId}/me/user-secrets/${secretId}`);
if (!res.ok) throw new Error('delete failed');

// after
const res = await api.delete(`/api/companies/${companyId}/me/user-secrets/${secretId}`);
if (res.status === 404) {
  logger.info(`secret ${secretId} already absent for this user; done`);
} else if (!res.ok) {
  throw new Error(`delete failed: ${res.status}`);
}
Defensive patterns

Strategy: fallback

Validate before calling

async function deleteMySecretValueSafe(api: ApiClient, companyId: string, secretId: string) {
  const res = await api.fetch(`/api/companies/${companyId}/me/user-secrets/${secretId}`, {
    method: 'DELETE',
  });
  if (res.status === 404) return { deleted: true, alreadyGone: true }; // idempotent
  if (!res.ok) throw new Error(`delete failed: ${res.status}`);
  return { deleted: true, alreadyGone: false };
}

Type guard

function isApiErrorBody(body: unknown): body is { error: string } {
  return typeof body === 'object' && body !== null &&
    typeof (body as Record<string, unknown>).error === 'string';
}
const isValueNotFound = (b: unknown): boolean =>
  isApiErrorBody(b) && b.error === 'User secret value not found';

Try / catch

try {
  await api.delete(`/api/companies/${companyId}/me/user-secrets/${secretId}`);
} catch (err) {
  if (err instanceof ApiError && err.status === 404 && isValueNotFound(err.body)) {
    return; // already gone or never owned by this user - success for removal intent
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18). Data as JSON: /api/errors/b65bd5de2b81191e. Report an issue: GitHub.

Appendix: source

Thrown at cli/src/commands/client/common.ts:128

export function resolveApiBase(options: Pick<BaseClientOptions, "apiBase" | "config">, profile: ClientContextProfile = {}): string {
  return normalizeApiBase(
    options.apiBase?.trim() ||
    process.env.PAPERCLIP_API_URL?.trim() ||
    profile.apiBase ||
    inferApiBaseFromConfig(options.config),
  );
}

export function normalizeApiBase(apiBase: string): string {
  return apiBase.trim().replace(/\/+$/, "");
}

export function apiPath(strings: TemplateStringsArray, ...values: Array<string | number | boolean | null | undefined>): string {
  let path = strings[0] ?? "";
  values.forEach((value, index) => {
    if (value === null || value === undefined || String(value).trim() === "") {
      throw new Error("Cannot build API path with an empty path segment.");
    }
    path += `${encodeURIComponent(String(value))}${strings[index + 1] ?? ""}`;
  });
  return path;
}

export function inferContentTypeFromPath(filePath: string): string | undefined {
  const ext = filePath.split(/[\\/]/).pop()?.split(".").pop()?.toLowerCase();
  if (!ext) return undefined;
  // These MIME strings are matched against the server's issue-attachment
  // allowlist (server/src/attachment-types.ts DEFAULT_ALLOWED_TYPES) by EXACT
  // string, so text types must carry no "; charset=..." parameter or the upload
  // is rejected with "422 Unsupported attachment content type". Keep this set in
  // sync with that allowlist (plus svg/avif, accepted by the asset routes).
  return {
    avif: "image/avif",
    csv: "text/csv",
    gif: "image/gif",

View on GitHub (pinned to 120ae5428f)