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:
- Add
MpakValidationError (4xx-class) and MpakServerError (5xx-class) to both SDKs' error hierarchies. Map ranges explicitly; keep MpakNotFoundError as a sub-class of MpakValidationError.
- 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.
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:BadRequestErrorfrom the registry — e.g. only one ofos/archsupplied), 401, 403, 500, 503 all classify the same.{ error: { message, code, statusCode } }is discarded.Python SDK — see
packages/sdk-python/src/mpak/client.py:get_server_download,get_bundle_downloadraiseMpakError(f"HTTP {status}: {body}", "HTTP_ERROR", status)for non-404 errors viahttpx.HTTPStatusError.codefrom the envelope is hardcoded to"HTTP_ERROR"regardless of the registry's actual code (VALIDATION_ERROR,RATE_LIMITED, etc.).Why it matters
--platformflag is wrong" from "registry is having a bad day."/servers/.../downloadendpoint whenosxorarchis supplied — Fastify validates againstBundleDownloadParamsSchemaand the route'sresolveArtifactalso throwsBadRequestError. The TS SDK reports this asMpakNetworkError: HTTP 400, which reads like a transient network issue.Proposed fix
Symmetric across both SDKs. Two viable paths:
MpakValidationError(4xx-class) andMpakServerError(5xx-class) to both SDKs' error hierarchies. Map ranges explicitly; keepMpakNotFoundErroras a sub-class ofMpakValidationError.MpakError(message, code, status)everywhere.codecarries the registry'scodefield; callers pattern-match oncoderather than class. Closer to Python's existing shape.Either way, every method's non-2xx branch wants the same translation function:
Scope
Affects ~6 methods in each SDK that go through
fetchWithTimeout(TS) orself._client.get/post(Python). Worth doing as one PR per SDK rather than per-method.Open questions
MpakValidationErrorcover 401/403 too, or split intoMpakAuthError?MpakErroralready hascodeandstatus_code— keep the existing class and addMpakValidationError(MpakError)as a sub-class so existingexcept MpakErrorkeeps working?except MpakNetworkErrorsince 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.