{"record":{"id":"69bd6d650d2e4507","repo":"unslothai/unsloth","slug":"invalid-block-id-only-uid-namespaced-blocks-can-b","errorCode":null,"errorMessage":"Invalid block_id: only uid-namespaced blocks can be deleted","messagePattern":"Invalid block_id: only uid-namespaced blocks can be deleted","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"studio/backend/routes/data_recipe/seed.py","lineNumber":597,"sourceCode":"        if not any(block_dir.iterdir()):\n            block_dir.rmdir()\n    except OSError:\n        pass\n\n    return {\"status\": \"ok\"}\n\n\n@router.delete(\"/seed/unstructured-block/{block_id}\")\nasync def remove_unstructured_block(block_id: str):\n    \"\"\"Delete a block's upload directory; files on disk still count toward its quota.\n\n    Only uid-namespaced directories may be bulk-deleted: they have exactly one\n    owning block. Legacy node-id directories (n1, ...) can be shared by other\n    recipes, so they are managed file-by-file instead.\n    \"\"\"\n    _validate_safe_id(block_id, \"block_id\")\n    if not _UPLOAD_UID_RE.match(block_id):\n        raise HTTPException(400, \"Invalid block_id: only uid-namespaced blocks can be deleted\")\n\n    block_dir = (UNSTRUCTURED_UPLOAD_ROOT / block_id).resolve()\n    if not block_dir.is_relative_to(UNSTRUCTURED_UPLOAD_ROOT.resolve()):\n        raise HTTPException(400, \"Invalid block_id: outside upload root\")\n    if not block_dir.exists():\n        return {\"status\": \"ok\", \"deleted\": False}\n\n    try:\n        shutil.rmtree(block_dir)\n    except OSError as exc:\n        raise log_and_http_error(\n            exc,\n            500,\n            \"failed to delete uploaded files\",\n            event = \"data_recipe.seed.unstructured_block_delete_failed\",\n            log = logger,\n        ) from exc\n    if block_dir.exists():","sourceCodeStart":579,"sourceCodeEnd":615,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/data_recipe/seed.py#L579-L615","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use the uid-namespaced block_id (32-char hex) returned when the upload block was created.","For legacy node-id blocks, delete files individually via DELETE /seed/unstructured-file/{block_id}/{file_id} instead of bulk-deleting the block.","Migrate old recipes to uid-namespaced blocks so bulk cleanup works."],"exampleFix":"# before\ndelete(f'/seed/unstructured-block/{node_id}')  # node_id='n1' -> 400\n\n# after\nfiles = list_files(node_id)\nfor f in files:\n    delete(f'/seed/unstructured-file/{node_id}/{f.file_id}')  # file-by-file for legacy blocks","handlingStrategy":"type-guard","validationCode":"const UID = /^[0-9a-f]{32}$/;\nconst canBulkDelete = UID.test(blockId);\nif (!canBulkDelete) { for (const f of await listBlockFiles(blockId)) await deleteFile(blockId, f.file_id); }\nelse { await deleteBlock(blockId); }","typeGuard":"const UPLOAD_UID_RE = /^[0-9a-f]{32}$/;\nfunction isUidNamespacedBlock(id: string): boolean {\n  return UPLOAD_UID_RE.test(id);\n}","tryCatchPattern":"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.","preventionTips":["Always create upload blocks with a fresh UUID4 hex namespace.","Keep node id and upload uid as distinct fields in frontend state so they are never swapped."],"tags":["upload","legacy","delete","http-400"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}