paperclipai/paperclip · error

Invalid JSON

Error message

Invalid ${name} JSON: ${err instanceof Error ? err.message : String(err)}

What it means

HTTP 404 with body {"error":"Provider vault not found"} from DELETE /api/secret-provider-configs/:id, second guard (secrets.ts:493). The preceding getAccessibleResource confirmed the provider config existed, but svc.removeProviderConfig(id) then returned null - the row was already gone by removal time (concurrent delete won the race). Board-only route.

Solutions

  1. Treat 404 on DELETE of a vault you already verified as success - the end state (vault gone) is achieved.
  2. Make cleanup scripts idempotent: fetch the list, delete each ID once, swallow 404s.
  3. Serialize destructive vault operations to avoid races with other admins.
  4. If the vault unexpectedly disappears, audit the activity log (secret_provider_config.removed) to find which actor deleted it.

Example fix

// before
const res = await api.delete(`/api/secret-provider-configs/${id}`);
if (!res.ok) throw new Error(`delete failed: ${res.status}`);

// after
const res = await api.delete(`/api/secret-provider-configs/${id}`);
if (res.status === 404) {
  logger.info(`vault ${id} already removed (race or earlier delete); success`);
} else if (!res.ok) {
  throw new Error(`delete failed: ${res.status}`);
}
Defensive patterns

Strategy: fallback

Validate before calling

async function listVaultIds(api: ApiClient): Promise<string[]> {
  const res = await api.fetch('/api/secret-provider-configs');
  if (!res.ok) throw new Error(`cannot list vaults: ${res.status}`);
  const list = await res.json();
  return (Array.isArray(list) ? list : list.items ?? []).map((v: { id: string }) => v.id);
}

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 isVaultNotFound = (b: unknown): boolean => isApiErrorBody(b) && b.error === 'Provider vault not found';

Try / catch

try {
  await api.delete(`/api/secret-provider-configs/${id}`);
} catch (err) {
  if (err instanceof ApiError && err.status === 404 && isVaultNotFound(err.body)) {
    return { deleted: true, alreadyGone: true }; // idempotent success
  }
  throw err;
}

Prevention

When it happens

Trigger: Two concurrent DELETE calls to the same vault config where the second passes the existence check but removes zero rows; a cleanup job and a manual operator deleting the same test vault simultaneously.

Common situations: Idempotency-unaware cleanup automation looping deletes; double-clicked delete buttons; provisioning scripts that remove default vaults while another pipeline recreates/removes them.

Related errors


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

Appendix: source

Thrown at cli/src/commands/client/approval.ts:258

      }),
  );
}

function parseCsv(value: string | undefined): string[] | undefined {
  if (!value) return undefined;
  const rows = value.split(",").map((v) => v.trim()).filter(Boolean);
  return rows.length > 0 ? rows : undefined;
}

function parseJsonObject(value: string, name: string): Record<string, unknown> {
  try {
    const parsed = JSON.parse(value) as unknown;
    if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
      throw new Error(`${name} must be a JSON object`);
    }
    return parsed as Record<string, unknown>;
  } catch (err) {
    throw new Error(`Invalid ${name} JSON: ${err instanceof Error ? err.message : String(err)}`);
  }
}

View on GitHub (pinned to 120ae5428f)