unslothai/unsloth · warning · HTTPException

Invalid version.

Error message

Invalid version.

What it means

HTTP 422 from GET /api/studio/release-notes when the 'version' query parameter fails is_supported_version_query(). That helper requires the string to match a safe version pattern and parse as a real version — 'latest', 'main', path-like strings, or garbage are rejected. The version is only echoed back (not used to look up a release), so this is purely shape validation to let the UI drop stale responses.

Source

Thrown at studio/backend/main.py:1746

    """Return source-aware install metadata without remote update checks."""
    return get_studio_install_source_status(UNSLOTH_VERSION)


@app.get("/api/studio/update-status")
def studio_update_status(_current_subject: str = Depends(get_current_subject)):
    """Return source-aware manual update status for browser-served Unsloth."""
    return get_studio_update_status(UNSLOTH_VERSION)


@app.get("/api/studio/release-notes")
def studio_release_notes(
    version: str = Query(..., max_length = 64),
    refresh: bool = Query(False),
    _current_subject: str = Depends(get_current_subject),
):
    """Return the newest release's notes. `version` is echoed, not looked up."""
    if not is_supported_version_query(version):
        raise HTTPException(status_code = 422, detail = "Invalid version.")
    return get_release_notes(version, refresh = refresh)


@app.get(
    "/api/studio/download-transport-capabilities",
    response_model = TransportCapabilities,
)
def studio_download_transport_capabilities(
    probe: bool = False, _current_subject: str = Depends(get_current_subject)
):
    # Sync def, so FastAPI runs this in the threadpool and an opted-in probe cannot block the loop.
    return asdict(get_download_transport_capabilities(probe = probe))


@app.post("/api/shutdown")
async def shutdown_server(request: Request, current_subject: str = Depends(get_current_subject)):
    """Gracefully shut down the Unsloth Studio server.

View on GitHub (pinned to 203007d190)

Solutions

  1. Pass a concrete version string shaped like a version (e.g. version=2025.8.1 or version=v1.2.3) that the update popup would actually offer.
  2. Do not send 'latest'/'main' — fetch the update status endpoint first and use the version it reports.
  3. If you control the client, validate the version before sending (regex for digits/dots/optional v-prefix).

Example fix

# before
GET /api/studio/release-notes?version=latest   # 422
# after
GET /api/studio/release-notes?version=2025.8.1  # 200
Defensive patterns

Strategy: validation

Validate before calling

import re

_VERSION_RE = re.compile(r"^v?\d+(\.\d+)*(-[A-Za-z0-9.]+)?$")

def is_supported_version_query(version: str) -> bool:
    c = version.strip()
    return len(c) <= 64 and bool(_VERSION_RE.match(c)) and c.lower() not in {"latest", "main"}

Type guard

def is_version_query(v: str) -> bool:
    """Narrow a string to a version the release-notes endpoint accepts."""
    return isinstance(v, str) and is_supported_version_query(v)

Prevention

When it happens

Trigger: Calling /api/studio/release-notes?version=latest, version=main, version=v (unparseable), a path like '../../etc', or any string with characters outside _SAFE_VERSION_PATTERN or longer than the 64-char Query cap.

Common situations: A frontend change that passes a branch name or 'latest' instead of a semver string; manual curl testing with placeholder values; a client built against an older API that accepted arbitrary strings.

Related errors


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