/** * Public error phrases — the ONLY text derived from a caught error that may * reach a wire response (`HealthStatus.poller.lastError`, `SyncRun.error`, * `SyncObjectProgress.error`; governed contract docs/api/worker.openapi.json). * * Allowlist keyed on error class / upstream HTTP status, mirroring the 502 * mapping in server.ts: a raw `err.message` is never copied out, because * Arena error bodies can echo request data (and, for the future S/4HANA leg, * OData error payloads can echo business data). Anything not on the list gets * the unclassified phrase; the full error stays in the worker log, which is * where diagnosis belongs (index.ts logs `{ name, status, message }`). * * `TimeoutError` is what Node's fetch rejects with when the request's * `AbortSignal.timeout(...)` fires (measured on this box, Node 24.15.0, * 2026-09-14: `name=TimeoutError`, a DOMException). */ import { ArenaApiError } from '../arena/session.js'; import { IngestBufferFullError } from '../poller/arenaDeltaPoll.js'; export const UNCLASSIFIED_ERROR_PHRASE = 'Unclassified worker error; see worker logs.'; export const INGEST_BUFFER_FULL_PHRASE = 'Ingest buffer full at poll time.'; export const ARENA_TIMEOUT_PHRASE = 'Upstream Arena PLM request timed out.'; export const ARENA_MALFORMED_PHRASE = 'Upstream Arena PLM response was malformed.'; export function arenaStatusPhrase(status: number): string { // A 2xx ArenaApiError is a response we could not interpret (e.g. login // answered 200 without an arenaSessionId), not an upstream failure code. if (status >= 200 && status < 300) return ARENA_MALFORMED_PHRASE; return `Upstream Arena PLM request failed (HTTP ${status}).`; } export function publicErrorPhrase(err: unknown): string { if (err instanceof ArenaApiError) return arenaStatusPhrase(err.status); if (err instanceof IngestBufferFullError) return INGEST_BUFFER_FULL_PHRASE; if (hasName(err, 'TimeoutError')) return ARENA_TIMEOUT_PHRASE; return UNCLASSIFIED_ERROR_PHRASE; } /** Log-safe summary of an error: class name, message, and upstream status if any. */ export function describeErrorForLog(err: unknown): { name: string; message: string; status?: number } { if (err instanceof ArenaApiError) return { name: err.name, message: err.message, status: err.status }; if (err instanceof Error) return { name: err.name, message: err.message }; return { name: typeof err, message: String(err) }; } function hasName(err: unknown, name: string): boolean { return typeof err === 'object' && err !== null && 'name' in err && (err as { name: unknown }).name === name; }