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

  1. List live slugs with GET /v1/workspaces and copy the exact slug from the response
  2. encodeURIComponent the slug when building the URL
  3. Treat 400 on this route as 'not found': make delete flows idempotent by accepting 400 as already-deleted
  4. 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

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


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)