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

  1. On 404, re-check GET /api/secret-provider-configs/:id; if also 404, drop the vault from the health-check roster.
  2. Pause or debounce health polls during planned provider-config maintenance windows.
  3. Distinguish this 404 (config record gone) from a real provider connectivity failure (route returns health payload with error details) before alerting.
  4. 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

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


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)