Skip to content

sdk(ts,python): non-404 HTTP errors lose status + envelope detail #116

@Ovaculos

Description

@Ovaculos

Both the TypeScript and Python SDKs collapse most non-2xx registry responses into a generic MpakNetworkError, dropping the registry's structured error envelope on the floor. Net effect: callers can distinguish "not found" from "everything else," but cannot tell a 400 (bad input) apart from a 503 (registry down) without parsing the error message string.

Current behavior

TypeScript SDK — see packages/sdk-typescript/src/client.ts:

  • getServerDownload, getBundleDownload, getServer, getSkill, etc. all use the pattern:
    if (response.status === 404) throw new MpakNotFoundError(...);
    if (!response.ok) throw new MpakNetworkError(`HTTP ${response.status}`);
  • 400 (BadRequestError from the registry — e.g. only one of os/arch supplied), 401, 403, 500, 503 all classify the same.
  • The registry's error body { error: { message, code, statusCode } } is discarded.

Python SDK — see packages/sdk-python/src/mpak/client.py:

  • Better, but inconsistent. get_server_download, get_bundle_download raise MpakError(f"HTTP {status}: {body}", "HTTP_ERROR", status) for non-404 errors via httpx.HTTPStatusError.
  • Status code is preserved, but code from the envelope is hardcoded to "HTTP_ERROR" regardless of the registry's actual code (VALIDATION_ERROR, RATE_LIMITED, etc.).

Why it matters

  • Retry logic: callers can't safely retry only on 5xx without string-parsing the message.
  • UX: a CLI that wraps the SDK can't distinguish "your --platform flag is wrong" from "registry is having a bad day."
  • 400 path is reachable in practice on the new /servers/.../download endpoint when os xor arch is supplied — Fastify validates against BundleDownloadParamsSchema and the route's resolveArtifact also throws BadRequestError. The TS SDK reports this as MpakNetworkError: HTTP 400, which reads like a transient network issue.

Proposed fix

Symmetric across both SDKs. Two viable paths:

  1. Add MpakValidationError (4xx-class) and MpakServerError (5xx-class) to both SDKs' error hierarchies. Map ranges explicitly; keep MpakNotFoundError as a sub-class of MpakValidationError.
  2. Parse the registry's envelope into a single MpakError(message, code, status) everywhere. code carries the registry's code field; callers pattern-match on code rather than class. Closer to Python's existing shape.

Either way, every method's non-2xx branch wants the same translation function:

async function translateError(response: Response, context: string): Promise<never> {
  const body = await response.json().catch(() => null);
  const code = body?.error?.code ?? 'HTTP_ERROR';
  const message = body?.error?.message ?? `HTTP ${response.status}`;
  if (response.status === 404) throw new MpakNotFoundError(context);
  if (response.status >= 400 && response.status < 500) {
    throw new MpakValidationError(message, code, response.status);
  }
  throw new MpakServerError(message, code, response.status);
}

Scope

Affects ~6 methods in each SDK that go through fetchWithTimeout (TS) or self._client.get/post (Python). Worth doing as one PR per SDK rather than per-method.

Open questions

  • Should MpakValidationError cover 401/403 too, or split into MpakAuthError?
  • Python's MpakError already has code and status_code — keep the existing class and add MpakValidationError(MpakError) as a sub-class so existing except MpakError keeps working?
  • Bump SDK majors on this, or treat as additive (new sub-classes don't break existing except MpakNetworkError since 5xx still raises a network-y error)?

Out of scope

This issue covers SDK error classification only. The registry already returns properly-structured error envelopes — no registry-side changes needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions