@ritas-inc/hanaqueryapi-client
Version:
TypeScript client for HANA Query API with full type safety and error handling
371 lines (325 loc) • 9.23 kB
text/typescript
/**
* HANA Query API Client - Error Classes
*
* Custom error classes for different types of client errors.
*/
import type {
ClientErrorType,
ClientErrorDetails,
ProblemDetails,
RequestContext
} from './types.ts';
/**
* Base client error class
*/
export class HanaQueryClientError extends Error {
public readonly type: ClientErrorType;
public readonly statusCode?: number;
public readonly originalError?: Error;
public readonly context?: RequestContext;
public readonly problemDetails?: ProblemDetails;
constructor(details: ClientErrorDetails) {
super(details.message);
this.name = 'HanaQueryClientError';
this.type = details.type;
this.statusCode = details.statusCode;
this.originalError = details.originalError;
this.context = details.context;
this.problemDetails = details.problemDetails;
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, HanaQueryClientError);
}
}
/**
* Convert error to JSON for logging/serialization
*/
toJSON(): Record<string, any> {
return {
name: this.name,
message: this.message,
type: this.type,
statusCode: this.statusCode,
context: this.context,
problemDetails: this.problemDetails,
stack: this.stack
};
}
/**
* Get a human-readable description of the error
*/
getDescription(): string {
const parts: string[] = [this.message];
if (this.statusCode) {
parts.push(`(HTTP ${this.statusCode})`);
}
if (this.context) {
const duration = this.context.duration ? `${this.context.duration}ms` : 'unknown';
parts.push(`[${this.context.method} ${this.context.url}] - ${duration}`);
}
return parts.join(' ');
}
}
/**
* Network-related errors (connection failures, timeouts, etc.)
*/
export class NetworkError extends HanaQueryClientError {
constructor(message: string, originalError?: Error, context?: RequestContext) {
super({
type: 'network_error',
message: `Network error: ${message}`,
originalError,
context
});
this.name = 'NetworkError';
}
}
/**
* Request timeout errors
*/
export class TimeoutError extends HanaQueryClientError {
constructor(timeout: number, context?: RequestContext) {
super({
type: 'timeout_error',
message: `Request timed out after ${timeout}ms`,
context
});
this.name = 'TimeoutError';
}
}
/**
* Validation errors (invalid parameters, etc.)
*/
export class ValidationError extends HanaQueryClientError {
constructor(message: string, issues?: string[], context?: RequestContext) {
const fullMessage = issues && issues.length > 0
? `${message}: ${issues.join(', ')}`
: message;
super({
type: 'validation_error',
message: `Validation error: ${fullMessage}`,
statusCode: 400,
context
});
this.name = 'ValidationError';
}
}
/**
* Authorization errors (401, 403)
*/
export class AuthorizationError extends HanaQueryClientError {
constructor(message: string, statusCode: number, context?: RequestContext) {
super({
type: 'authorization_error',
message: `Authorization error: ${message}`,
statusCode,
context
});
this.name = 'AuthorizationError';
}
}
/**
* Not found errors (404)
*/
export class NotFoundError extends HanaQueryClientError {
constructor(resource: string, context?: RequestContext) {
super({
type: 'not_found_error',
message: `Resource not found: ${resource}`,
statusCode: 404,
context
});
this.name = 'NotFoundError';
}
}
/**
* Server errors (5xx)
*/
export class ServerError extends HanaQueryClientError {
constructor(message: string, statusCode: number, problemDetails?: ProblemDetails, context?: RequestContext) {
super({
type: 'server_error',
message: `Server error: ${message}`,
statusCode,
problemDetails,
context
});
this.name = 'ServerError';
}
}
/**
* Unknown/unexpected errors
*/
export class UnknownError extends HanaQueryClientError {
constructor(message: string, originalError?: Error, context?: RequestContext) {
super({
type: 'unknown_error',
message: `Unknown error: ${message}`,
originalError,
context
});
this.name = 'UnknownError';
}
}
/**
* Create appropriate error from HTTP response
*/
export function createErrorFromResponse(
response: Response,
responseData: any,
context?: RequestContext
): HanaQueryClientError {
const { status, statusText } = response;
// Handle API error responses with problem details
if (responseData && typeof responseData === 'object' && responseData.problem) {
const problem = responseData.problem as ProblemDetails;
switch (status) {
case 400:
return new ValidationError(
problem.detail || 'Bad request',
problem.issues,
context
);
case 401:
case 403:
return new AuthorizationError(
problem.detail || statusText || 'Unauthorized',
status,
context
);
case 404:
return new NotFoundError(
problem.detail || statusText || 'Not found',
context
);
case 500:
case 502:
case 503:
case 504:
return new ServerError(
problem.detail || statusText || 'Server error',
status,
problem,
context
);
default:
return new UnknownError(
problem.detail || `HTTP ${status}: ${statusText}`,
undefined,
context
);
}
}
// Handle HTTP errors without API problem details
switch (status) {
case 400:
return new ValidationError('Bad request', undefined, context);
case 401:
case 403:
return new AuthorizationError(
statusText || 'Unauthorized',
status,
context
);
case 404:
return new NotFoundError(statusText || 'Not found', context);
case 500:
case 502:
case 503:
case 504:
return new ServerError(
statusText || 'Server error',
status,
undefined,
context
);
default:
return new UnknownError(
`HTTP ${status}: ${statusText}`,
undefined,
context
);
}
}
/**
* Create error from network/fetch failure
*/
export function createNetworkError(
error: Error,
context?: RequestContext
): HanaQueryClientError {
const message = error.message.toLowerCase();
// Check for timeout
if (message.includes('timeout') || error.name === 'TimeoutError') {
return new TimeoutError(context?.duration || 0, context);
}
// Check for network issues
if (
message.includes('network') ||
message.includes('fetch') ||
message.includes('connection') ||
error.name === 'TypeError' ||
error.name === 'AbortError'
) {
return new NetworkError(error.message, error, context);
}
// Unknown error
return new UnknownError(error.message, error, context);
}
/**
* Check if an error is retryable
*/
export function isRetryableError(error: HanaQueryClientError): boolean {
switch (error.type) {
case 'network_error':
case 'timeout_error':
return true;
case 'server_error':
// Retry 5xx errors except for 501 (Not Implemented)
return error.statusCode !== 501;
case 'authorization_error':
case 'validation_error':
case 'not_found_error':
return false;
case 'unknown_error':
default:
return false;
}
}
/**
* Get retry delay for an error (exponential backoff)
*/
export function getRetryDelay(
attempt: number,
baseDelay: number,
maxDelay: number,
backoffMultiplier: number = 2
): number {
const delay = baseDelay * Math.pow(backoffMultiplier, attempt - 1);
return Math.min(delay, maxDelay);
}
/**
* Type guards for specific error types
*/
export function isNetworkError(error: any): error is NetworkError {
return error instanceof NetworkError || error?.type === 'network_error';
}
export function isTimeoutError(error: any): error is TimeoutError {
return error instanceof TimeoutError || error?.type === 'timeout_error';
}
export function isValidationError(error: any): error is ValidationError {
return error instanceof ValidationError || error?.type === 'validation_error';
}
export function isAuthorizationError(error: any): error is AuthorizationError {
return error instanceof AuthorizationError || error?.type === 'authorization_error';
}
export function isNotFoundError(error: any): error is NotFoundError {
return error instanceof NotFoundError || error?.type === 'not_found_error';
}
export function isServerError(error: any): error is ServerError {
return error instanceof ServerError || error?.type === 'server_error';
}
export function isHanaQueryClientError(error: any): error is HanaQueryClientError {
return error instanceof HanaQueryClientError ||
(error && typeof error.type === 'string' && error.type.endsWith('_error'));
}