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
- On 404, re-fetch GET /api/secret-provider-configs/:id - if also 404, the vault was deleted; choose a different vault to promote.
- Coordinate destructive and default-promotion operations (do them in one place, e.g. one admin or one pipeline).
- After any default change, re-read the provider config list to learn the current default instead of assuming your POST succeeded.
- 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
- Centralize default-vault promotion and vault deletion in one workflow so they cannot race.
- Re-read the vault list after promotion to confirm which vault is default.
- During provider migrations, pause default-promotion jobs until cutover completes.
- Prefer promoting a vault you just confirmed via GET in the same second, and handle 404 as 'choose again'.
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
- Invalid JSON
- must be a JSON object
- Request failed with status
- 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/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)