Repository navigation
Expand file tree
/
Copy pathKernelErrorMapping.ts
More file actions
353 lines (326 loc) · 13.2 KB
/
Copy pathKernelErrorMapping.ts
File metadata and controls
353 lines (326 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
import { constants } from 'node:os';
import { getSystemErrorName } from 'node:util';
import HiveDriverError from '../errors/HiveDriverError';
import AuthenticationError from '../errors/AuthenticationError';
import OperationStateError, { OperationStateErrorCode } from '../errors/OperationStateError';
import ParameterError from '../errors/ParameterError';
/**
* Sentinel prefix the napi binding's `napi_err_from_kernel` puts on
* `Error.message` when the underlying failure was a structured kernel
* `Error` rather than a plain napi `InvalidArg` from binding-side
* validation. Defined here (and in `native/kernel/src/error.rs:44`) — the
* two MUST stay in lockstep.
*/
const ERROR_SENTINEL = '__databricks_error__:';
/**
* Shape of the kernel error surfaced by the napi-binding's `napi_err_from_kernel`.
*
* The Rust kernel's `kernel_error::Error` is exposed as a `JsError` whose
* properties mirror the Rust struct: the `ErrorCode` variant name (as a string),
* the message, and an optional SQLSTATE (either taken from the structured
* server response or recovered via `extract_sqlstate_from_message`).
*/
export interface KernelErrorShape {
/** Kernel `ErrorCode` variant name, e.g. `"Unauthenticated"`, `"SqlError"`. */
code: string;
/** Human-readable error message. */
message: string;
/** Optional SQLSTATE — five-char alphanumeric, when the kernel was able to surface it. */
sqlstate?: string;
}
/**
* Kernel `ErrorCode` variants — the 13 variants of the `#[non_exhaustive]` enum
* defined in `src/kernel_error.rs:66-134`.
*
* Kept here as a literal type rather than an `enum` so test exhaustiveness checks
* and runtime `code` strings are guaranteed to stay in lockstep with the kernel.
*/
export type KernelErrorCode =
| 'InvalidArgument'
| 'Unauthenticated'
| 'PermissionDenied'
| 'NotFound'
| 'ResourceExhausted'
| 'Unavailable'
| 'Timeout'
| 'Cancelled'
| 'DataLoss'
| 'Internal'
| 'InvalidStatementHandle'
| 'NetworkError'
| 'SqlError';
/**
* Optional metadata fields the kernel may attach via the
* `__databricks_error__:` envelope (per `native/kernel/src/error.rs:50-89`).
*
* `errorCode` is namespaced under `kernelMetadata` rather than placed at
* the top level because `OperationStateError` already declares a top-level
* `errorCode: enum` field, and `DBSQLOperation.ts` switches on it
* (`err.errorCode === OperationStateErrorCode.Canceled`, around `:374`). Top-level
* defineProperty would clobber that enum with a kernel string and break
* cancel/close detection.
*/
export interface KernelMetadata {
errorCode?: string;
vendorCode?: number;
httpStatus?: number;
retryable?: boolean;
queryId?: string;
/**
* Rich server-error diagnostics from the kernel terminal-error payload. These
* mirror the fields the **Thrift** backend already surfaces via
* `OperationStateError.response` (`TGetOperationStatusResp.displayMessage` /
* `.errorMessage` / `.errorDetailsJson`) and that the Python `use_kernel`
* connector forwards onto its exceptions — so kernel stays at parity rather
* than dropping them. Kernel sources: `Error.display_message` /
* `.diagnostic_info` / `.error_details_json`.
*/
displayMessage?: string;
diagnosticInfo?: string;
errorDetailsJson?: string;
}
/**
* An `Error` carrying optional kernel-side kernel context. `sqlState` is
* exposed at the top level (no collision in the existing driver error
* tree); the remaining envelope fields live under a `kernelMetadata`
* namespace to avoid clobbering pre-existing `errorCode` semantics on
* `OperationStateError`.
*/
export interface ErrorWithSqlState extends Error {
sqlState?: string;
kernelMetadata?: KernelMetadata;
}
/**
* Attach a non-enumerable own-property to the error. The shape matches
* Node's convention for attaching `.code` to system errors:
* non-enumerable (clean `JSON.stringify`) but readable via direct
* property access and `Object.getOwnPropertyDescriptor`. One helper for
* both the top-level `sqlState` and the namespaced `kernelMetadata`
* object so the `defineProperty` flags live in exactly one place.
*/
function defineErrorMetadata<K extends string, V>(error: Error, key: K, value: V): void {
Object.defineProperty(error, key, {
value,
writable: true,
enumerable: false,
configurable: true,
});
}
/**
* Map a kernel error (as surfaced by the napi-binding) to the appropriate JS
* driver error class.
*
* M0 mapping table:
* Unauthenticated, PermissionDenied → AuthenticationError
* Cancelled → OperationStateError(Canceled)
* Timeout → OperationStateError(Timeout)
* InvalidArgument → ParameterError
* NetworkError, Unavailable,
* NotFound, ResourceExhausted,
* DataLoss, Internal,
* InvalidStatementHandle, SqlError → HiveDriverError
*
* Unknown `code` values (e.g. if the kernel adds a new variant) fall through
* to HiveDriverError so the driver never silently drops an error. The kernel's
* `ErrorCode` is `#[non_exhaustive]` so this can legitimately happen.
*
* SQLSTATE, when present, is attached on `error.sqlState` regardless of which
* class is returned.
*/
export function mapKernelErrorToJsError(kErr: KernelErrorShape): ErrorWithSqlState {
const { code, message, sqlstate } = kErr;
let error: ErrorWithSqlState;
switch (code as KernelErrorCode) {
case 'Unauthenticated':
case 'PermissionDenied':
error = new AuthenticationError(message);
break;
case 'Cancelled':
// OperationStateError with the Canceled code carries the kernel message
// through the response.displayMessage fallback path.
error = new OperationStateError(OperationStateErrorCode.Canceled);
error.message = message;
break;
case 'Timeout':
error = new OperationStateError(OperationStateErrorCode.Timeout);
error.message = message;
break;
case 'InvalidArgument':
error = new ParameterError(message);
break;
case 'SqlError': {
// A server-reported SQL execution failure (kernel `SqlError`, e.g. a
// bad query, missing table, divide-by-zero, invalid cast). The Thrift
// backend surfaces the same situation as `OperationStateError(Error)`
// when the operation reaches ERROR_STATE (see DBSQLOperation), so map
// SqlError to the same class for backend parity. OperationStateError
// extends HiveDriverError, so existing `instanceof HiveDriverError`
// catches are unaffected.
const stateError = new OperationStateError(OperationStateErrorCode.Error);
stateError.message = message;
error = stateError;
break;
}
// All remaining kernel ErrorCode variants map to the base driver error class.
// M0 intentionally does not introduce new error classes; M1 may add nuance.
case 'NotFound':
case 'ResourceExhausted':
case 'Unavailable':
case 'DataLoss':
case 'Internal':
case 'InvalidStatementHandle':
case 'NetworkError':
error = new HiveDriverError(message);
break;
default:
// Unknown/future kernel variant — never drop the error, surface as base class.
error = new HiveDriverError(message);
break;
}
if (sqlstate !== undefined) {
defineErrorMetadata(error, 'sqlState', sqlstate);
}
return error;
}
/**
* Build a {@link KernelMetadata} object from a parsed envelope, applying
* per-field type validation. A kernel-side bug that emits, say,
* `retryable: "true"` (string) instead of `true` (boolean) would
* otherwise leak the wrong-typed value through to JS callers; the
* type-guard discards the malformed field rather than passing it through.
*/
function buildKernelMetadata(parsed: Record<string, unknown>): KernelMetadata {
const meta: KernelMetadata = {};
if (typeof parsed.errorCode === 'string') {
meta.errorCode = parsed.errorCode;
}
if (typeof parsed.vendorCode === 'number') {
meta.vendorCode = parsed.vendorCode;
}
if (typeof parsed.httpStatus === 'number') {
meta.httpStatus = parsed.httpStatus;
}
if (typeof parsed.retryable === 'boolean') {
meta.retryable = parsed.retryable;
}
if (typeof parsed.queryId === 'string') {
meta.queryId = parsed.queryId;
}
// Rich diagnostics — Thrift/Python parity (were previously dropped).
if (typeof parsed.displayMessage === 'string') {
meta.displayMessage = parsed.displayMessage;
}
if (typeof parsed.diagnosticInfo === 'string') {
meta.diagnosticInfo = parsed.diagnosticInfo;
}
if (typeof parsed.errorDetailsJson === 'string') {
meta.errorDetailsJson = parsed.errorDetailsJson;
}
return meta;
}
const MAX_CAUSE_MESSAGE_LENGTH = 500;
function boundCauseMessage(text: string): string {
return text.length > MAX_CAUSE_MESSAGE_LENGTH ? `${text.slice(0, MAX_CAUSE_MESSAGE_LENGTH - 1)}…` : text;
}
/**
* Decode a napi-binding error into the typed JS error class.
*
* Two paths:
* - Structured kernel error: `Error.message` starts with
* {@link ERROR_SENTINEL} followed by a JSON envelope. We strip the
* sentinel, parse the JSON, route the {@link KernelErrorShape}
* through {@link mapKernelErrorToJsError}, and attach the remaining
* envelope fields under a single non-enumerable `kernelMetadata`
* namespace. Namespacing avoids the collision with
* `OperationStateError.errorCode` (an enum already switched on at the
* JS layer — see `DBSQLOperation.ts` around `:374`).
* - Binding-side error (e.g. `napi::Error::new(InvalidArg, "openSession:
* \`token\` is required for the requested auth mode")` produced by
* the binding's own validation): returned unchanged. These don't
* carry kernel `code` info, so we surface them as-is.
*
* Non-`Error` values (e.g. a `Promise.reject('string')`) pass through
* wrapped in `HiveDriverError` so callers always see an `Error`
* subclass.
*/
export function decodeNapiKernelError(err: unknown): Error {
if (!(err instanceof Error)) {
return new HiveDriverError(typeof err === 'string' ? err : 'kernel backend: unknown error');
}
const { message } = err;
if (typeof message !== 'string' || !message.startsWith(ERROR_SENTINEL)) {
return err;
}
const jsonStr = message.slice(ERROR_SENTINEL.length);
let parsed: unknown;
try {
parsed = JSON.parse(jsonStr);
} catch {
// Corrupted envelope — surface the raw post-sentinel payload rather
// than silently dropping the original error. Strip the internal
// `__databricks_error__:` prefix; it's a binding/JS-side framing
// marker, not user-actionable, and leaking it makes the message
// confusing to operators triaging a malformed kernel response.
//
// Mutate in place when possible so the napi-binding's original
// stack survives — that stack is the only useful triage signal on
// a malformed-envelope path (where did a sentinel-prefixed
// non-JSON message come from?). Fall back to a fresh
// `HiveDriverError` only if a future napi-rs revision makes
// `Error.message` non-writable (no such guarantee today, but the
// descriptor contract is implementation-defined).
try {
err.message = jsonStr;
return err;
} catch {
return new HiveDriverError(jsonStr);
}
}
if (
typeof parsed !== 'object' ||
parsed === null ||
typeof (parsed as { code?: unknown }).code !== 'string' ||
typeof (parsed as { message?: unknown }).message !== 'string'
) {
return err;
}
const envelope = parsed as Record<string, unknown>;
const code = envelope.code as string;
const msg = envelope.message as string;
const sqlState = typeof envelope.sqlState === 'string' ? envelope.sqlState : undefined;
let jsErr = mapKernelErrorToJsError({ code, message: msg, sqlstate: sqlState });
if (typeof envelope.localPath === 'string' && typeof envelope.localErrorKind === 'string') {
const kindCodes: Record<string, string> = {
NotFound: 'ENOENT',
PermissionDenied: 'EACCES',
AlreadyExists: 'EEXIST',
NotADirectory: 'ENOTDIR',
IsADirectory: 'EISDIR',
DirectoryNotEmpty: 'ENOTEMPTY',
};
const raw = typeof envelope.localOsError === 'number' ? envelope.localOsError : undefined;
const localCode =
process.platform !== 'win32' && raw
? getSystemErrorName(-Math.abs(raw))
: kindCodes[envelope.localErrorKind] ?? 'EIO';
jsErr = Object.assign(new Error(msg), {
code: localCode,
errno: -((constants.errno as Record<string, number>)[localCode] ?? Math.abs(raw ?? constants.errno.EIO)),
path: envelope.localPath,
});
}
const meta = buildKernelMetadata(envelope);
// Skip the namespace attachment entirely when no fields validated
// through — keeps `err.kernelMetadata` absent rather than `{}` for
// simple envelopes (the common case). Key-count check so new
// `KernelMetadata` fields are covered automatically.
if (Object.keys(meta).length > 0) {
defineErrorMetadata(jsErr, 'kernelMetadata', meta);
}
// Attach a bounded summary of the kernel error as `cause` (same descriptor as a
// native `new Error(msg, { cause })`). The binding error itself is not used: its
// message is the whole envelope, including the unbounded diagnostics, which
// `util.inspect` would print for every kernel error.
defineErrorMetadata(jsErr, 'cause', new Error(boundCauseMessage(`${code}: ${msg}`)));
return jsErr;
}