Mintplex-Labs/anything-llm · warning
Bad Request
Error message
Bad Request
What it means
DELETE /v1/workspace/:slug resolves the slug via `Workspace.get({slug})`; when no workspace matches, the handler intentionally returns `response.sendStatus(400)` — AnythingLLM uses 400 Bad Request (not 404) for 'workspace slug not found' on this route. This is a client-side addressing error: the slug in the URL does not exist (never existed, was already deleted, or arrived URL-mangled). Deletion protection (WORKSPACE_DELETION_PROTECTION) is a different failure (403) and never produces this 400.
Solutions
- List live slugs with GET /v1/workspaces and copy the exact slug from the response
- encodeURIComponent the slug when building the URL
- Treat 400 on this route as 'not found': make delete flows idempotent by accepting 400 as already-deleted
- If deletion must be impossible-by-policy, expect 403 from WORKSPACE_DELETION_PROTECTION instead and remove that env to allow deletes
Example fix
// before
await fetch(`${base}/api/v1/workspace/my workspace`, {method:'DELETE', headers});
// after
const slugs = (await fetch(`${base}/api/v1/workspaces`, opts).then(r=>r.json())).workspaces.map(w=>w.slug);
if (!slugs.includes('my workspace')) throw new Error('slug not found - nothing to delete');
await fetch(`${base}/api/v1/workspace/${encodeURIComponent('my workspace')}`, {method:'DELETE', headers}); Defensive patterns
Strategy: validation
Validate before calling
const live = new Set((await fetch(`${base}/api/v1/workspaces`, opts).then(r=>r.json())).workspaces.map(w=>w.slug));
if (!live.has(slug)) return 'already-deleted'; // 400 is this API's not-found Type guard
const isDeletableSlug = (s) => typeof s === 'string' && s.trim().length > 0 && s === s.trim();
Try / catch
try { const r = await del(workspaceUrl(slug)); if (r.status===400) return {ok:true, note:'not found = nothing to delete'}; if (r.status===403) throw new Error('deletion protection enabled'); } catch (e) { throw e; } Prevention
- Fetch the slug fresh before every delete - renames change slugs
- Treat 400 as not-found and make deletes idempotent
- Expect 403 when WORKSPACE_DELETION_PROTECTION is set
When it happens
Trigger: Deleting with a typo'd or stale slug; double-deleting (second call finds nothing); a slug with special characters sent unencoded so Prisma matches a literal '%20'-style string; slug case mismatch since lookup is exact.
Common situations: Scripts that cache slugs across workspace recreations; UI/automation race where the workspace was renamed (new slug) between listing and deleting; trailing whitespace or slashes in the URL path.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- Workspace not found
- Workspace or thread is not valid.
- File not found
- Internal Server Error
- Message is empty
AI-assisted analysis of Mintplex-Labs/anything-llm@3aec848f28 (2026-08-18).
Data as JSON: /api/errors/01e22d64672b8166.
Report an issue: GitHub.
Appendix: source
Thrown at server/endpoints/api/workspace/index.js:253
#swagger.parameters['slug'] = {
in: 'path',
description: 'Unique slug of workspace to delete',
required: true,
type: 'string'
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
*/
try {
const { slug = "" } = request.params;
const VectorDb = getVectorDbClass();
const workspace = await Workspace.get({ slug: String(slug) });
if (!workspace) {
response.sendStatus(400).end();
return;
}
const workspaceId = Number(workspace.id);
await WorkspaceChats.delete({ workspaceId: workspaceId });
await DocumentVectors.deleteForWorkspace(workspaceId);
await Document.delete({ workspaceId: workspaceId });
await Workspace.delete({ id: workspaceId });
await EventLogs.logEvent("api_workspace_deleted", {
workspaceName: workspace?.name || "Unknown Workspace",
});
try {
await VectorDb["delete-namespace"]({ namespace: slug });
} catch (e) {
console.error(e.message);
}
response.sendStatus(200).end();View on GitHub (pinned to 3aec848f28)