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
- Treat 404 on DELETE of a vault you already verified as success - the end state (vault gone) is achieved.
- Make cleanup scripts idempotent: fetch the list, delete each ID once, swallow 404s.
- Serialize destructive vault operations to avoid races with other admins.
- 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
- Code DELETE 404 as success in all vault cleanup automation.
- Drive deletions from a fresh list call each cycle, deleting only present IDs.
- Coordinate with other admins so deletes and edits do not interleave.
- After deletes, re-read the vault list to confirm final state instead of assuming.
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
- Cannot build API path with an empty path segment.
- Challenge secret is required. Pass --token or --token-env.
- --file is required
- must be a JSON object
- Request failed with status
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)