unslothai/unsloth · error · HTTPException

Invalid block_id: only uid-namespaced blocks can be deleted

Error message

Invalid block_id: only uid-namespaced blocks can be deleted

What it means

HTTP 400 raised by DELETE /seed/unstructured-block/{block_id} when block_id passes the safe-id check but is not a 32-char lowercase hex uid (_UPLOAD_UID_RE, the UUID4-hex namespace the frontend generates). Legacy node-id directories (n1, n2, ...) can be shared by several recipes, so bulk deletion is deliberately restricted to uid-namespaced blocks, which have exactly one owner.

Source

Thrown at studio/backend/routes/data_recipe/seed.py:597

        if not any(block_dir.iterdir()):
            block_dir.rmdir()
    except OSError:
        pass

    return {"status": "ok"}


@router.delete("/seed/unstructured-block/{block_id}")
async def remove_unstructured_block(block_id: str):
    """Delete a block's upload directory; files on disk still count toward its quota.

    Only uid-namespaced directories may be bulk-deleted: they have exactly one
    owning block. Legacy node-id directories (n1, ...) can be shared by other
    recipes, so they are managed file-by-file instead.
    """
    _validate_safe_id(block_id, "block_id")
    if not _UPLOAD_UID_RE.match(block_id):
        raise HTTPException(400, "Invalid block_id: only uid-namespaced blocks can be deleted")

    block_dir = (UNSTRUCTURED_UPLOAD_ROOT / block_id).resolve()
    if not block_dir.is_relative_to(UNSTRUCTURED_UPLOAD_ROOT.resolve()):
        raise HTTPException(400, "Invalid block_id: outside upload root")
    if not block_dir.exists():
        return {"status": "ok", "deleted": False}

    try:
        shutil.rmtree(block_dir)
    except OSError as exc:
        raise log_and_http_error(
            exc,
            500,
            "failed to delete uploaded files",
            event = "data_recipe.seed.unstructured_block_delete_failed",
            log = logger,
        ) from exc
    if block_dir.exists():

View on GitHub (pinned to 203007d190)

Solutions

  1. Use the uid-namespaced block_id (32-char hex) returned when the upload block was created.
  2. For legacy node-id blocks, delete files individually via DELETE /seed/unstructured-file/{block_id}/{file_id} instead of bulk-deleting the block.
  3. Migrate old recipes to uid-namespaced blocks so bulk cleanup works.

Example fix

# before
delete(f'/seed/unstructured-block/{node_id}')  # node_id='n1' -> 400

# after
files = list_files(node_id)
for f in files:
    delete(f'/seed/unstructured-file/{node_id}/{f.file_id}')  # file-by-file for legacy blocks
Defensive patterns

Strategy: type-guard

Validate before calling

const UID = /^[0-9a-f]{32}$/;
const canBulkDelete = UID.test(blockId);
if (!canBulkDelete) { for (const f of await listBlockFiles(blockId)) await deleteFile(blockId, f.file_id); }
else { await deleteBlock(blockId); }

Type guard

const UPLOAD_UID_RE = /^[0-9a-f]{32}$/;
function isUidNamespacedBlock(id: string): boolean {
  return UPLOAD_UID_RE.test(id);
}

Try / catch

Catch the 400 and branch: if the id is legacy, fall back to per-file deletes; otherwise fix the caller to pass the uid block id.

Prevention

When it happens

Trigger: Calling block delete with a legacy id like 'n1' or any non-UUID4-hex id; using a recipe node id instead of the upload namespace uid as block_id.

Common situations: Older recipes/frontend versions that stored uploads under node ids; new code path mixing up node id and upload uid; manual API calls constructed from node ids.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/69bd6d650d2e4507. Report an issue: GitHub.