Skip to content

Commit 1bc717d

Browse files
author
Transcript-Fetch-Admin
committed
feat: typed error hierarchy
1 parent c47c8c9 commit 1bc717d

1 file changed

Lines changed: 123 additions & 0 deletions

File tree

‎src/errors.ts‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Error hierarchy for the TranscriptFetch SDK.
3+
*
4+
* The API returns a canonical error envelope:
5+
* `{ ok: false, request_id, error: { code, message, issues? } }`. We map
6+
* `error.code` (falling back to the HTTP status) to a specific subclass so
7+
* callers can branch cleanly with `instanceof`.
8+
*/
9+
10+
/** Base class for every error raised by this SDK. */
11+
export class TranscriptFetchError extends Error {
12+
constructor(message: string) {
13+
super(message);
14+
this.name = new.target.name;
15+
// Restore the prototype chain (needed when targeting ES5-era down-levels).
16+
Object.setPrototypeOf(this, new.target.prototype);
17+
}
18+
}
19+
20+
/** A network problem prevented the request from completing (DNS/TLS/connect). */
21+
export class APIConnectionError extends TranscriptFetchError {}
22+
23+
/** The request did not complete within the configured timeout. */
24+
export class APITimeoutError extends APIConnectionError {}
25+
26+
export interface APIErrorInit {
27+
status: number;
28+
code?: string | null;
29+
requestId?: string | null;
30+
issues?: unknown[];
31+
}
32+
33+
/** The API returned an error response. */
34+
export class APIError extends TranscriptFetchError {
35+
readonly status: number;
36+
readonly code: string | null;
37+
readonly requestId: string | null;
38+
readonly issues: unknown[];
39+
40+
constructor(message: string, init: APIErrorInit) {
41+
super(message);
42+
this.status = init.status;
43+
this.code = init.code ?? null;
44+
this.requestId = init.requestId ?? null;
45+
this.issues = init.issues ?? [];
46+
}
47+
}
48+
49+
/** 401: missing or invalid API key. */
50+
export class AuthenticationError extends APIError {}
51+
/** 400: the request body failed validation (see `issues`). */
52+
export class InvalidRequestError extends APIError {}
53+
/** 402: not enough credits to complete the request. */
54+
export class InsufficientCreditsError extends APIError {}
55+
/** 409: Idempotency-Key reused with a different body, or still in flight. */
56+
export class IdempotencyConflictError extends APIError {}
57+
58+
/** 429: per-key rate limit exceeded. */
59+
export class RateLimitError extends APIError {
60+
/** Seconds to wait before retrying, parsed from the Retry-After header when present. */
61+
readonly retryAfter: number | null;
62+
63+
constructor(message: string, init: APIErrorInit & { retryAfter?: number | null }) {
64+
super(message, init);
65+
this.retryAfter = init.retryAfter ?? null;
66+
}
67+
}
68+
69+
/** 502/503: the upstream transcript service was unreachable. Safe to retry. */
70+
export class UpstreamUnavailableError extends APIError {}
71+
/** 500: unexpected server error. */
72+
export class InternalServerError extends APIError {}
73+
74+
const CODE_TO_EXC: Record<string, new (message: string, init: APIErrorInit) => APIError> = {
75+
unauthorized: AuthenticationError,
76+
invalid_request: InvalidRequestError,
77+
insufficient_credits: InsufficientCreditsError,
78+
idempotency_conflict: IdempotencyConflictError,
79+
rate_limited: RateLimitError,
80+
upstream_unavailable: UpstreamUnavailableError,
81+
internal_error: InternalServerError,
82+
};
83+
84+
const STATUS_TO_EXC: Record<number, new (message: string, init: APIErrorInit) => APIError> = {
85+
400: InvalidRequestError,
86+
401: AuthenticationError,
87+
402: InsufficientCreditsError,
88+
409: IdempotencyConflictError,
89+
429: RateLimitError,
90+
500: InternalServerError,
91+
502: UpstreamUnavailableError,
92+
503: UpstreamUnavailableError,
93+
};
94+
95+
interface ErrorEnvelope {
96+
error?: { code?: string; message?: string; issues?: unknown[] };
97+
}
98+
99+
/** Map an error response to the appropriate exception and throw it. */
100+
export function raiseApiError(
101+
status: number,
102+
payload: ErrorEnvelope,
103+
requestId: string | null,
104+
retryAfter?: string | null,
105+
): never {
106+
const error = payload && typeof payload === "object" ? payload.error : undefined;
107+
const code = error?.code ?? null;
108+
const message = error?.message || `Request failed with status ${status}`;
109+
const issues = Array.isArray(error?.issues) ? error!.issues : [];
110+
111+
const ctor = (code && CODE_TO_EXC[code]) || STATUS_TO_EXC[status] || APIError;
112+
113+
if (ctor === RateLimitError) {
114+
let parsed: number | null = null;
115+
if (retryAfter != null) {
116+
const n = Number(retryAfter);
117+
parsed = Number.isFinite(n) ? n : null;
118+
}
119+
throw new RateLimitError(message, { status, code, requestId, issues, retryAfter: parsed });
120+
}
121+
122+
throw new ctor(message, { status, code, requestId, issues });
123+
}

0 commit comments

Comments
 (0)