paperclipai/paperclip · error

--file is required

Error message

--file is required

What it means

HTTP 404 with body {"error":"Provider vault not found"} from POST /api/secret-provider-configs/:id/default, second guard (secrets.ts:522). getAccessibleResource already proved the config existed and was company-accessible; svc.setDefaultProviderConfig(id) returning null means the config vanished before the default flag could be set - a concurrent delete (or removal via another mutation) raced this request. Board-only route.

Solutions

  1. On 404, re-fetch GET /api/secret-provider-configs/:id - if also 404, the vault was deleted; choose a different vault to promote.
  2. Coordinate destructive and default-promotion operations (do them in one place, e.g. one admin or one pipeline).
  3. After any default change, re-read the provider config list to learn the current default instead of assuming your POST succeeded.
  4. Retry the promotion with a surviving vault ID if the deleted one was replaced.

Example fix

// before
await api.post(`/api/secret-provider-configs/${id}/default`, {});

// after
let res = await api.post(`/api/secret-provider-configs/${id}/default`, {});
if (res.status === 404) {
  const vaults = await api.listProviderConfigs();
  const candidate = vaults.find((v) => v.status === 'active');
  if (!candidate) throw new Error('no active vault to promote');
  res = await api.post(`/api/secret-provider-configs/${candidate.id}/default`, {});
}
Defensive patterns

Strategy: validation

Validate before calling

async function promoteDefaultSafe(api: ApiClient, id: string) {
  const existing = await api.fetch(`/api/secret-provider-configs/${id}`);
  if (existing.status === 404) throw new Error(`vault ${id} missing`);
  const res = await api.fetch(`/api/secret-provider-configs/${id}/default`, { method: 'POST' });
  if (res.status === 404) {
    // concurrent delete raced us: pick another active vault
    const list = await api.fetch('/api/secret-provider-configs').then((r) => r.json());
    const fallback = (Array.isArray(list) ? list : list.items ?? []).find(
      (v: { id: string; status: string }) => v.id !== id && v.status === 'active',
    );
    if (!fallback) throw new Error('no surviving vault to promote');
    return api.fetch(`/api/secret-provider-configs/${fallback.id}/default`, { method: 'POST' });
  }
  return res;
}

Type guard

function isApiErrorBody(body: unknown): body is { error: string } {
  return typeof body === 'object' && body !== null &&
    typeof (body as Record<string, unknown>).error === 'string';
}

Try / catch

try {
  await api.post(`/api/secret-provider-configs/${id}/default`, {});
} catch (err) {
  if (err instanceof ApiError && err.status === 404 && err.body?.error === 'Provider vault not found') {
    throw new VaultVanishedError(id); // re-select default from fresh list
  }
  throw err;
}

Prevention

When it happens

Trigger: Marking a vault as default at the same moment another board user deletes it; a cleanup script removing test vaults while an operator promotes one to default; two admins switching defaults across overlapping vault sets.

Common situations: Environment teardown racing configuration changes; onboarding flows that create a vault, set it default, and delete the previous default while other clients still reference it.

Related errors


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

Appendix: source

Thrown at cli/src/commands/client/asset.ts:95

            printOutput({ ok: true, out: opts.out, bytes: bytes.length }, { json: ctx.json });
            return;
          }
          process.stdout.write(bytes);
        } catch (err) {
          handleCommandError(err);
        }
      }),
  );
}

async function uploadAsset(
  apiBase: string,
  apiKey: string | undefined,
  path: string,
  opts: AssetOptions,
): Promise<unknown> {
  if (!opts.file?.trim()) {
    throw new Error("--file is required");
  }
  const bytes = await readFile(opts.file);
  const form = new FormData();
  form.set("file", new Blob([bytes], { type: inferContentTypeFromPath(opts.file) }), opts.file.split(/[\\/]/).pop() ?? "asset");
  if (opts.namespace?.trim()) form.set("namespace", opts.namespace.trim());
  if (opts.alt?.trim()) form.set("alt", opts.alt.trim());
  if (opts.title?.trim()) form.set("title", opts.title.trim());

  const response = await fetch(buildApiUrl(apiBase, path), {
    method: "POST",
    headers: apiKey ? { authorization: `Bearer ${apiKey}` } : undefined,
    body: form,
  });
  return parseFetchResponse(response);
}

async function downloadAsset(apiBase: string, apiKey: string | undefined, assetId: string): Promise<Buffer> {
  const response = await fetch(buildApiUrl(apiBase, apiPath`/api/assets/${assetId}/content`), {

View on GitHub (pinned to 120ae5428f)