paperclipai/paperclip · error
Cannot build API path with an empty path segment.
Error message
Cannot build API path with an empty path segment.
What it means
HTTP 404 with body {"error":"User secret value not found"} from DELETE /api/companies/:companyId/me/user-secrets/:secretId (secrets.ts:856). svc.removeCurrentUserSecretValue(companyId, ownerUserId, secretId) returned null: no value row exists for this (company, current user, secretId) triple at removal time - already deleted, owned by a different user, or a bad ID. Route requires board auth plus company access, and the value must belong to the calling user.
Solutions
- Treat 404 on DELETE as already-removed (idempotent success) when removal is the goal.
- Drive deletions from the current user's own secret list, refreshed immediately before the call.
- Ensure the authenticated board user is the value's owner before scripting deletes.
- Disable delete controls after first click and re-sync the list from the response.
Example fix
// before
const res = await api.delete(`/api/companies/${companyId}/me/user-secrets/${secretId}`);
if (!res.ok) throw new Error('delete failed');
// after
const res = await api.delete(`/api/companies/${companyId}/me/user-secrets/${secretId}`);
if (res.status === 404) {
logger.info(`secret ${secretId} already absent for this user; done`);
} else if (!res.ok) {
throw new Error(`delete failed: ${res.status}`);
} Defensive patterns
Strategy: fallback
Validate before calling
async function deleteMySecretValueSafe(api: ApiClient, companyId: string, secretId: string) {
const res = await api.fetch(`/api/companies/${companyId}/me/user-secrets/${secretId}`, {
method: 'DELETE',
});
if (res.status === 404) return { deleted: true, alreadyGone: true }; // idempotent
if (!res.ok) throw new Error(`delete failed: ${res.status}`);
return { deleted: true, alreadyGone: false };
} 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 isValueNotFound = (b: unknown): boolean =>
isApiErrorBody(b) && b.error === 'User secret value not found'; Try / catch
try {
await api.delete(`/api/companies/${companyId}/me/user-secrets/${secretId}`);
} catch (err) {
if (err instanceof ApiError && err.status === 404 && isValueNotFound(err.body)) {
return; // already gone or never owned by this user - success for removal intent
}
throw err;
} Prevention
- Treat 404 on personal secret DELETE as success in cleanup flows.
- Source deletion targets from the current user's own secret list, fetched just before.
- Ensure the authenticated board user matches the value's owner in shared tooling.
- Re-sync the secret list from delete responses to keep UI state truthful.
When it happens
Trigger: Double-delete of the same personal secret value; deleting a value the current user never had (e.g. another user's secretId); retrying a delete that already succeeded; deleting after switching board user context.
Common situations: Settings pages with double-submitted delete actions; test scripts cycling multiple user accounts against a shared list of secret IDs; deprovisioning helpers that run after the value was already cleared.
Related errors
- Challenge secret is required. Pass --token or --token-env.
- Company ID is required. Pass --company-id, set…
- Invalid JSON
- Could not locate local Paperclip skills directory. Expected…
- Environment variable
AI-assisted analysis of paperclipai/paperclip@120ae5428f (2026-08-18).
Data as JSON: /api/errors/b65bd5de2b81191e.
Report an issue: GitHub.
Appendix: source
Thrown at cli/src/commands/client/common.ts:128
export function resolveApiBase(options: Pick<BaseClientOptions, "apiBase" | "config">, profile: ClientContextProfile = {}): string {
return normalizeApiBase(
options.apiBase?.trim() ||
process.env.PAPERCLIP_API_URL?.trim() ||
profile.apiBase ||
inferApiBaseFromConfig(options.config),
);
}
export function normalizeApiBase(apiBase: string): string {
return apiBase.trim().replace(/\/+$/, "");
}
export function apiPath(strings: TemplateStringsArray, ...values: Array<string | number | boolean | null | undefined>): string {
let path = strings[0] ?? "";
values.forEach((value, index) => {
if (value === null || value === undefined || String(value).trim() === "") {
throw new Error("Cannot build API path with an empty path segment.");
}
path += `${encodeURIComponent(String(value))}${strings[index + 1] ?? ""}`;
});
return path;
}
export function inferContentTypeFromPath(filePath: string): string | undefined {
const ext = filePath.split(/[\\/]/).pop()?.split(".").pop()?.toLowerCase();
if (!ext) return undefined;
// These MIME strings are matched against the server's issue-attachment
// allowlist (server/src/attachment-types.ts DEFAULT_ALLOWED_TYPES) by EXACT
// string, so text types must carry no "; charset=..." parameter or the upload
// is rejected with "422 Unsupported attachment content type". Keep this set in
// sync with that allowlist (plus svg/avif, accepted by the asset routes).
return {
avif: "image/avif",
csv: "text/csv",
gif: "image/gif",View on GitHub (pinned to 120ae5428f)