(err: unknown)
| 94 | const CH_SERVER_ERROR_PREFIX = /^Code:\s*\d+/; |
| 95 | |
| 96 | export function classifyError(err: unknown): ErrorClass { |
| 97 | if (!err || typeof err !== 'object') { |
| 98 | return 'other'; |
| 99 | } |
| 100 | const e = err as { |
| 101 | message?: string; |
| 102 | code?: unknown; |
| 103 | cause?: unknown; |
| 104 | type?: unknown; |
| 105 | }; |
| 106 | const msg = String(e.message ?? ''); |
| 107 | |
| 108 | // ── Deny: ClickHouse server errors ──────────────────────────────────── |
| 109 | if (CH_SERVER_ERROR_PREFIX.test(msg)) { |
| 110 | return 'ch-server'; |
| 111 | } |
| 112 | if (typeof e.type === 'string' && e.type.startsWith('DB::')) { |
| 113 | return 'ch-server'; |
| 114 | } |
| 115 | |
| 116 | // ── Explicit error.code wins ────────────────────────────────────────── |
| 117 | if (typeof e.code === 'string') { |
| 118 | if (NODE_DOWN_CODES.has(e.code)) return 'node-down'; |
| 119 | if (TRANSIENT_CODES.has(e.code)) return 'transient'; |
| 120 | } |
| 121 | |
| 122 | // ── Message keyword fallback ────────────────────────────────────────── |
| 123 | if (NODE_DOWN_MESSAGE_KEYWORDS.some((k) => msg.includes(k))) { |
| 124 | return 'node-down'; |
| 125 | } |
| 126 | if (TRANSIENT_MESSAGE_KEYWORDS.some((k) => msg.includes(k))) { |
| 127 | return 'transient'; |
| 128 | } |
| 129 | |
| 130 | // ── Bare-code substring fallback ────────────────────────────────────── |
| 131 | if (NODE_DOWN_BARE_CODE_REGEX.test(msg)) return 'node-down'; |
| 132 | if (TRANSIENT_BARE_CODE_REGEX.test(msg)) return 'transient'; |
| 133 | |
| 134 | // ── Wrapped causes (Node 18+ AggregateError, undici wrapping) ───────── |
| 135 | if (e.cause) { |
| 136 | return classifyError(e.cause); |
| 137 | } |
| 138 | |
| 139 | return 'other'; |
| 140 | } |
| 141 | |
| 142 | /** Convenience: should we retry this error at all (regardless of whether we |
| 143 | * sin-bin the node)? */ |
no test coverage detected