|
| 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