paperclipai/paperclip · error · ApiRequestError
Request failed with status
Error message
Request failed with status ${response.status} What it means
HTTP 404 with body {"error":"Provider vault not found"} from POST /api/secret-provider-configs/:id/health, second guard (secrets.ts:551). The vault passed the earlier getAccessibleResource existence/tenant check, but svc.checkProviderConfigHealth(id) returned null - the provider config row was no longer readable when the health probe ran, almost always because a concurrent DELETE removed it in between. Board-only route; note this 404 is about the config record, not the external provider being unhealthy.
Solutions
- On 404, re-check GET /api/secret-provider-configs/:id; if also 404, drop the vault from the health-check roster.
- Pause or debounce health polls during planned provider-config maintenance windows.
- Distinguish this 404 (config record gone) from a real provider connectivity failure (route returns health payload with error details) before alerting.
- Keep the monitored vault list derived from a fresh list call each cycle instead of a static config.
Example fix
// before
const health = await api.post(`/api/secret-provider-configs/${id}/health`, {});
// after
const vault = await api.get(`/api/secret-provider-configs/${id}`);
if (!vault) {
monitoredVaultIds.delete(id); // vault removed; stop probing
return null;
}
const health = await api.post(`/api/secret-provider-configs/${id}/health`, {}); Defensive patterns
Strategy: validation
Validate before calling
async function healthCheckRoster(api: ApiClient): Promise<string[]> {
const res = await api.fetch('/api/secret-provider-configs');
const list = await res.json();
return (Array.isArray(list) ? list : list.items ?? []).map((v: { id: string }) => v.id);
}
// probe only IDs returned by healthCheckRoster() this cycle 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 {
const health = await api.post(`/api/secret-provider-configs/${id}/health`, {});
return health;
} catch (err) {
if (err instanceof ApiError && err.status === 404 && isVaultNotFound(err.body)) {
dropFromRoster(id); // vault deleted mid-cycle; stop probing
return null;
}
throw err;
} Prevention
- Rebuild the monitored vault list from the API every polling cycle.
- Do not conflate this 404 with provider unhealthiness - it means the config record is gone.
- Pause health sweeps during vault maintenance windows to avoid noise.
- Auto-retire vault IDs from monitoring after a 404; they never reappear with the same ID.
When it happens
Trigger: Health-checking a vault while another admin deletes it; periodic health pollers hitting vaults mid-teardown; running health sweeps during provider migration where old configs are removed as new ones are created.
Common situations: Monitoring dashboards polling all vaults on an interval racing config cleanup; smoke tests after deploy referencing a vault that decommissioning removed.
Related errors
- --file is required
- Invalid JSON
- must be a JSON object
- Cannot build API path with an empty path segment.
- Challenge secret is required. Pass --token or --token-env.
AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18).
Data as JSON: /api/errors/c0c381d19b02ecc8.
Report an issue: GitHub.
Appendix: source
Thrown at cli/src/commands/client/asset.ts:130
async function downloadAsset(apiBase: string, apiKey: string | undefined, assetId: string): Promise<Buffer> {
const response = await fetch(buildApiUrl(apiBase, apiPath`/api/assets/${assetId}/content`), {
headers: apiKey ? { authorization: `Bearer ${apiKey}` } : undefined,
});
if (!response.ok) {
await parseFetchResponse(response);
}
return Buffer.from(await response.arrayBuffer());
}
async function parseFetchResponse(response: Response): Promise<unknown> {
const text = await response.text();
const parsed = text.trim() ? safeJson(text) : null;
if (!response.ok) {
const message =
typeof parsed === "object" && parsed !== null && "error" in parsed && typeof parsed.error === "string"
? parsed.error
: `Request failed with status ${response.status}`;
throw new ApiRequestError(response.status, message, undefined, parsed);
}
return parsed;
}
function buildApiUrl(apiBase: string, path: string): string {
const url = new URL(apiBase);
url.pathname = `${url.pathname.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
return url.toString();
}
function safeJson(text: string): unknown {
try {
return JSON.parse(text);
} catch {
return text;
}
}
View on GitHub (pinned to 120ae5428f)