Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 19 additions & 61 deletions packages/core/src/apierror/apierror.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {z} from 'zod';

import {Code, codeFromString} from './codes';
import {Code} from './codes';
import type {ErrorDetails} from './details';
import {parseErrorDetails} from './details';

Expand Down Expand Up @@ -43,7 +43,9 @@ interface ApiErrorOptions {

/** ApiError is a transport-agnostic error representing a Databricks API error. */
export class ApiError extends Error {
/** The canonical error code of the error. */
/**
* The error code of the error.
*/
readonly code: Code;

/**
Expand Down Expand Up @@ -130,7 +132,7 @@ export class ApiError extends Error {

if (body === undefined || body.length === 0) {
return new ApiError({
code: toCode(statusCode),
code: Code.UNKNOWN,
message: '',
details: emptyDetails,
httpStatusCode: statusCode,
Expand All @@ -148,7 +150,7 @@ export class ApiError extends Error {
// error does not come directly from a Databricks API. A typical example
// is when the error is returned by a proxy.
return new ApiError({
code: toCode(statusCode),
code: Code.UNKNOWN,
message: '',
details: emptyDetails,
httpStatusCode: statusCode,
Expand All @@ -161,7 +163,7 @@ export class ApiError extends Error {
const result = errorResponseSchema.safeParse(parsed);
if (!result.success) {
return new ApiError({
code: toCode(statusCode),
code: Code.UNKNOWN,
message: '',
details: emptyDetails,
httpStatusCode: statusCode,
Expand All @@ -173,15 +175,17 @@ export class ApiError extends Error {

const errResp = result.data;

// Error codes may be missing or be an integer (legacy APIs). In such
// cases, defer to the HTTP status code to infer the closest canonical
// error code.
let errorCode: Code;
if (typeof errResp.error_code === 'string') {
errorCode = codeFromString(errResp.error_code);
} else {
errorCode = toCode(statusCode);
}
// code carries the error_code string verbatim: a canonical code (e.g.
// "NOT_FOUND") matches a named Code member, while a Databricks
// product-specific code (e.g. "CATALOG_DOES_NOT_EXIST") is an open Code
// value. It is Code.UNKNOWN when the response carries no string error_code
// (missing or an integer); the HTTP status is never used to infer a code,
// since it may not reflect the true error semantic, so callers fall back to
// httpStatusCode.
const code: Code =
typeof errResp.error_code === 'string' && errResp.error_code !== ''
? errResp.error_code
: Code.UNKNOWN;

// Determine the error message from available fields.
let errorMessage = '';
Expand All @@ -196,7 +200,7 @@ export class ApiError extends Error {
}

return new ApiError({
code: errorCode,
code,
message: errorMessage,
details: parseErrorDetails(errResp.details),
httpStatusCode: statusCode,
Expand All @@ -205,49 +209,3 @@ export class ApiError extends Error {
});
}
}

// Maps an HTTP status code to the closest canonical error code.
export function toCode(httpCode: number): Code {
// Canonical mappings.
switch (httpCode) {
case 200:
return Code.OK;
case 400:
return Code.INVALID_ARGUMENT;
case 401:
return Code.UNAUTHENTICATED;
case 403:
return Code.PERMISSION_DENIED;
case 404:
return Code.NOT_FOUND;
case 409:
return Code.ABORTED;
case 416:
return Code.OUT_OF_RANGE;
case 429:
return Code.RESOURCE_EXHAUSTED;
case 501:
return Code.UNIMPLEMENTED;
case 503:
return Code.UNAVAILABLE;
case 504:
return Code.DEADLINE_EXCEEDED;
default:
break;
}

// Fallback for status codes without a direct canonical mapping.
if (httpCode >= 200 && httpCode < 300) {
return Code.OK;
}
if (httpCode >= 400 && httpCode < 500) {
// Most non-canonical 4xx status codes are state related and map
// to the definition of FailedPrecondition.
return Code.FAILED_PRECONDITION;
}
if (httpCode >= 500 && httpCode < 600) {
return Code.INTERNAL;
}

return Code.UNKNOWN;
}
106 changes: 26 additions & 80 deletions packages/core/src/apierror/codes/codes.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
/**
* Defines error codes for API errors and their retry semantics.
*
* @packageDocumentation
* Code is the error code carried by an API error.
*/

/**
* Code is a numeric code for an error.
*
* The numeric values are stable and can be relied upon across SDK versions.
*/
enum Code {
// eslint-disable-next-line @typescript-eslint/naming-convention -- Enum-style const object.
export const Code = {
/**
* Unknown indicates an error that cannot be classified.
*
* This code might be used for malformed error responses or error responses
* using an error code that cannot be mapped to a code in this package.
* Unknown indicates an error that cannot be classified. It is used for
* malformed error responses and for responses that carry no string error
* code (missing or an integer).
*/
UNKNOWN = 0,
UNKNOWN: 'UNKNOWN',

/** OK indicates the operation completed successfully. */
OK = 1,
OK: 'OK',

/** Canceled indicates the operation was canceled (typically by the caller). */
CANCELED = 2,
/** Cancelled indicates the operation was cancelled (typically by the caller). */
CANCELLED: 'CANCELLED',

/**
* InvalidArgument indicates the client specified an invalid argument.
Expand All @@ -31,7 +23,7 @@ enum Code {
* that are problematic regardless of the state of the system. For example,
* a malformed request parameter.
*/
INVALID_ARGUMENT = 3,
INVALID_ARGUMENT: 'INVALID_ARGUMENT',

/**
* DeadlineExceeded means the operation expired before completion.
Expand All @@ -41,19 +33,19 @@ enum Code {
* example, a successful response from a server could have been delayed
* long enough for the deadline to expire.
*/
DEADLINE_EXCEEDED = 4,
DEADLINE_EXCEEDED: 'DEADLINE_EXCEEDED',

/**
* NotFound means a requested entity (e.g. a resource or a file) was
* not found.
*/
NOT_FOUND = 5,
NOT_FOUND: 'NOT_FOUND',

/**
* AlreadyExists means an attempt to create an entity failed because one
* already exists.
*/
ALREADY_EXISTS = 6,
ALREADY_EXISTS: 'ALREADY_EXISTS',

/**
* PermissionDenied indicates the caller does not have permission to
Expand All @@ -63,28 +55,28 @@ enum Code {
* some resource (e.g. too many requests) which is a ResourceExhausted
* error.
*/
PERMISSION_DENIED = 7,
PERMISSION_DENIED: 'PERMISSION_DENIED',

/**
* ResourceExhausted indicates some resource has been exhausted, perhaps
* a per-user quota, or perhaps the entire file system is out of space.
*/
RESOURCE_EXHAUSTED = 8,
RESOURCE_EXHAUSTED: 'RESOURCE_EXHAUSTED',

/**
* FailedPrecondition indicates the operation was rejected because the
* system is not in a state required for the operation's execution.
* For example, directory to be deleted may be non-empty, an rmdir
* operation is applied to a non-directory, etc.
*/
FAILED_PRECONDITION = 9,
FAILED_PRECONDITION: 'FAILED_PRECONDITION',

/**
* Aborted indicates the operation was aborted, typically due to a
* concurrency issue like sequencer check failures, transaction aborts,
* etc.
*/
ABORTED = 10,
ABORTED: 'ABORTED',

/**
* OutOfRange means the operation was attempted past the valid range.
Expand All @@ -103,20 +95,20 @@ enum Code {
* a space can easily look for an OutOfRange error to detect when
* they are done.
*/
OUT_OF_RANGE = 11,
OUT_OF_RANGE: 'OUT_OF_RANGE',

/**
* Unimplemented indicates the operation is not implemented or not
* supported/enabled in this service.
*/
UNIMPLEMENTED = 12,
UNIMPLEMENTED: 'UNIMPLEMENTED',

/**
* Internal indicates an internal error. This means some invariants
* expected by the underlying system have been broken. If you see
* this error, something is very broken.
*/
INTERNAL = 13,
INTERNAL: 'INTERNAL',

/**
* Unavailable indicates the service is currently unavailable.
Expand All @@ -128,62 +120,16 @@ enum Code {
* The Databricks SDK will generally automatically retry the request
* with a backoff when encountering this error.
*/
UNAVAILABLE = 14,
UNAVAILABLE: 'UNAVAILABLE',

/** DataLoss indicates unrecoverable data loss or corruption. */
DATA_LOSS = 15,
DATA_LOSS: 'DATA_LOSS',

/**
* Unauthenticated indicates the request does not have valid
* authentication credentials for the operation.
*/
UNAUTHENTICATED = 16,
}

// Maps Code values to their canonical string representation.
const CODE_TO_STRING: ReadonlyMap<Code, string> = new Map([
[Code.UNKNOWN, 'UNKNOWN'],
[Code.OK, 'OK'],
[Code.CANCELED, 'CANCELLED'],
[Code.INVALID_ARGUMENT, 'INVALID_ARGUMENT'],
[Code.DEADLINE_EXCEEDED, 'DEADLINE_EXCEEDED'],
[Code.NOT_FOUND, 'NOT_FOUND'],
[Code.ALREADY_EXISTS, 'ALREADY_EXISTS'],
[Code.PERMISSION_DENIED, 'PERMISSION_DENIED'],
[Code.RESOURCE_EXHAUSTED, 'RESOURCE_EXHAUSTED'],
[Code.FAILED_PRECONDITION, 'FAILED_PRECONDITION'],
[Code.ABORTED, 'ABORTED'],
[Code.OUT_OF_RANGE, 'OUT_OF_RANGE'],
[Code.UNIMPLEMENTED, 'UNIMPLEMENTED'],
[Code.INTERNAL, 'INTERNAL'],
[Code.UNAVAILABLE, 'UNAVAILABLE'],
[Code.DATA_LOSS, 'DATA_LOSS'],
[Code.UNAUTHENTICATED, 'UNAUTHENTICATED'],
]);

// Maps canonical strings back to Code values.
const STRING_TO_CODE: ReadonlyMap<string, Code> = new Map(
[...CODE_TO_STRING.entries()].map(([code, str]) => [str, code])
);

/**
* Returns the canonical string representation of an error code.
*
* If the code is not recognized, "UNKNOWN" is returned. Note that
* Code.CANCELED maps to "CANCELLED" (British spelling) to match the gRPC
* convention.
*/
function codeToString(code: Code): string {
return CODE_TO_STRING.get(code) ?? 'UNKNOWN';
}

/**
* Converts a string representation of an error code to its corresponding
* Code value. If the string does not match any known code, Code.UNKNOWN is
* returned.
*/
function codeFromString(s: string): Code {
return STRING_TO_CODE.get(s) ?? Code.UNKNOWN;
}
UNAUTHENTICATED: 'UNAUTHENTICATED',
} as const;

export {Code, codeToString, codeFromString};
export type Code = (typeof Code)[keyof typeof Code] | (string & {});
2 changes: 1 addition & 1 deletion packages/core/src/apierror/codes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
* @packageDocumentation
*/

export {Code, codeToString, codeFromString} from './codes';
export {Code} from './codes';
Loading
Loading