unslothai/unsloth · error · HTTPException

Invalid {label}: must be alphanumeric/dash/underscore only

Error message

Invalid {label}: must be alphanumeric/dash/underscore only

What it means

HTTP 400 raised by _validate_safe_id in the seed upload routes when a block_id or file_id fails the regex ^[a-zA-Z0-9_-]+$ (empty string or any character outside letters, digits, dash, underscore). The guard exists because these ids are interpolated into filesystem paths under the upload root, so it doubles as path-traversal protection.

Source

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

logger = get_logger(__name__)
router = APIRouter()

DATA_EXTS = (".parquet", ".jsonl", ".json", ".csv")
DEFAULT_SPLIT = "train"
LOCAL_UPLOAD_EXTS = {".csv", ".json", ".jsonl"}
UNSTRUCTURED_ALLOWED_EXTS = {".pdf", ".docx", ".txt", ".md"}
SEED_UPLOAD_DIR = seed_uploads_root()
UNSTRUCTURED_UPLOAD_ROOT = unstructured_uploads_root()
_SAFE_ID_RE = re.compile(r"^[a-zA-Z0-9_-]+$")
# Frontend-generated upload namespace (UUID4 hex). Legacy node ids (n1, ...)
# never match: those directories can be shared by several recipes.
_UPLOAD_UID_RE = re.compile(r"^[0-9a-f]{32}$")


def _validate_safe_id(value: str, label: str) -> str:
    if not value or not _SAFE_ID_RE.match(value):
        raise HTTPException(400, f"Invalid {label}: must be alphanumeric/dash/underscore only")
    return value


def _serialize_preview_value(value: Any) -> Any:
    return to_preview_jsonable(value)


def _serialize_preview_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
    return [
        {str(key): _serialize_preview_value(value) for key, value in row.items()} for row in rows
    ]


def _normalize_optional_text(value: str | None) -> str | None:
    if value is None:
        return None
    trimmed = value.strip()
    return trimmed if trimmed else None

View on GitHub (pinned to 203007d190)

Solutions

  1. Generate ids client-side from [a-zA-Z0-9_-] only (the frontend upload namespace is a UUID4 hex, which always passes).
  2. Sanitize before sending: strip or replace invalid characters in block_id/file_id.
  3. If you intended a legacy node id (n1, n2), keep it alphanumeric — no dots or slashes.
  4. Check the actual request payload in devtools to find which field carries the bad character.

Example fix

// before
const blockId = `block/${recipeId}:${nodeId}`; // slashes/colons rejected

// after
const blockId = crypto.randomUUID().replace(/-/g, ''); // 32-hex uid, always valid
Defensive patterns

Strategy: validation

Validate before calling

const SAFE_ID = /^[a-zA-Z0-9_-]+$/;
function isValidSafeId(v) { return typeof v === 'string' && SAFE_ID.test(v); }
if (!isValidSafeId(blockId)) throw new Error(`invalid block_id: ${blockId}`);

Type guard

const SAFE_ID_RE = /^[a-zA-Z0-9_-]+$/;
function isSafeId(v: unknown): v is string {
  return typeof v === 'string' && v.length > 0 && SAFE_ID_RE.test(v);
}

Try / catch

Wrap the upload/inspect fetch; on 400 with 'Invalid ... alphanumeric' in the detail, surface a form error naming the offending field instead of retrying.

Prevention

When it happens

Trigger: POST /seed/upload-unstructured-file with block_id containing a dot, slash, space, or unicode char; DELETE /seed/unstructured-file/{block_id}/{file_id} with a URL-encoded path segment like %2e%2e; any seed endpoint taking block_id/file_id form or path params with invalid characters.

Common situations: Frontend sends a node id like 'n.1' or a namespaced id with a colon; user-supplied filename used as an id; proxy stripping/decoding URL components; test fixtures using arbitrary strings.

Related errors


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