UNPKG

pubnub

Version:

Publish & Subscribe Real-time Messaging with PubNub

293 lines (264 loc) 10.9 kB
/** * REST API endpoint use error module. * * @internal */ import { TransportResponse } from '../core/types/transport-response'; import RequestOperation from '../core/constants/operations'; import StatusCategory from '../core/constants/categories'; import { Payload, Status } from '../core/types/api'; import { PubNubError } from './pubnub-error'; /** * PubNub REST API call error. * * @internal */ export class PubNubAPIError extends Error { /** * Construct API from known error object or {@link PubNub} service error response. * * @param errorOrResponse - `Error` or service error response object from which error information * should be extracted. * @param [data] - Preprocessed service error response. * * @returns `PubNubAPIError` object with known error category and additional information (if * available). */ static create(errorOrResponse: Error | TransportResponse, data?: ArrayBuffer): PubNubAPIError { if (PubNubAPIError.isErrorObject(errorOrResponse)) return PubNubAPIError.createFromError(errorOrResponse); else return PubNubAPIError.createFromServiceResponse(errorOrResponse, data); } /** * Create API error instance from other error object. * * @param error - `Error` object provided by network provider (mostly) or other {@link PubNub} client components. * * @returns `PubNubAPIError` object with known error category and additional information (if * available). */ private static createFromError(error: unknown): PubNubAPIError { let category: StatusCategory = StatusCategory.PNUnknownCategory; let message = 'Unknown error'; let errorName = 'Error'; if (!error) return new PubNubAPIError(message, category, 0); else if (error instanceof PubNubAPIError) return error; if (PubNubAPIError.isErrorObject(error)) { message = error.message; errorName = error.name; } if (errorName === 'AbortError' || message.indexOf('Aborted') !== -1) { category = StatusCategory.PNCancelledCategory; message = 'Request cancelled'; } else if (message.toLowerCase().indexOf('timeout') !== -1) { category = StatusCategory.PNTimeoutCategory; message = 'Request timeout'; } else if (message.toLowerCase().indexOf('network') !== -1) { category = StatusCategory.PNNetworkIssuesCategory; message = 'Network issues'; } else if (errorName === 'TypeError') { if (message.indexOf('Load failed') !== -1 || message.indexOf('Failed to fetch') != -1) category = StatusCategory.PNNetworkIssuesCategory; else category = StatusCategory.PNBadRequestCategory; } else if (errorName === 'FetchError') { const errorCode = (error as Record<string, string>).code; if (['ECONNREFUSED', 'ENETUNREACH', 'ENOTFOUND', 'ECONNRESET', 'EAI_AGAIN'].includes(errorCode)) category = StatusCategory.PNNetworkIssuesCategory; if (errorCode === 'ECONNREFUSED') message = 'Connection refused'; else if (errorCode === 'ENETUNREACH') message = 'Network not reachable'; else if (errorCode === 'ENOTFOUND') message = 'Server not found'; else if (errorCode === 'ECONNRESET') message = 'Connection reset by peer'; else if (errorCode === 'EAI_AGAIN') message = 'Name resolution error'; else if (errorCode === 'ETIMEDOUT') { category = StatusCategory.PNTimeoutCategory; message = 'Request timeout'; } else message = `Unknown system error: ${error}`; } else if (message === 'Request timeout') category = StatusCategory.PNTimeoutCategory; return new PubNubAPIError(message, category, 0, error as Error); } /** * Construct API from known {@link PubNub} service error response. * * @param response - Service error response object from which error information should be * extracted. * @param [data] - Preprocessed service error response. * * @returns `PubNubAPIError` object with known error category and additional information (if * available). */ private static createFromServiceResponse(response: TransportResponse, data?: ArrayBuffer): PubNubAPIError { let category: StatusCategory = StatusCategory.PNUnknownCategory; let errorData: Error | Payload | undefined; let message = 'Unknown error'; let { status } = response; data ??= response.body; if (status === 402) message = 'Not available for used key set. Contact support@pubnub.com'; else if (status === 400) { category = StatusCategory.PNBadRequestCategory; message = 'Bad request'; } else if (status === 403) { category = StatusCategory.PNAccessDeniedCategory; message = 'Access denied'; } else if (status >= 500) { category = StatusCategory.PNServerErrorCategory; message = 'Internal server error'; } if (typeof response === 'object' && Object.keys(response).length === 0) { category = StatusCategory.PNMalformedResponseCategory; message = 'Malformed response (network issues)'; status = 400; } // Try to get more information about error from service response. if (data && data.byteLength > 0) { const decoded = new TextDecoder().decode(data); if ( response.headers['content-type']!.indexOf('text/javascript') !== -1 || response.headers['content-type']!.indexOf('application/json') !== -1 ) { try { const errorResponse: Payload = JSON.parse(decoded); if (typeof errorResponse === 'object') { if (!Array.isArray(errorResponse)) { if ( 'error' in errorResponse && (errorResponse.error === 1 || errorResponse.error === true) && 'status' in errorResponse && typeof errorResponse.status === 'number' && 'message' in errorResponse && 'service' in errorResponse ) { errorData = errorResponse; status = errorResponse.status; } else errorData = errorResponse; if ('error' in errorResponse && errorResponse.error instanceof Error) errorData = errorResponse.error; } else { // Handling Publish API payload error. if (typeof errorResponse[0] === 'number' && errorResponse[0] === 0) { if (errorResponse.length > 1 && typeof errorResponse[1] === 'string') errorData = errorResponse[1]; } } } } catch (_) { errorData = decoded; } } else if (response.headers['content-type']!.indexOf('xml') !== -1) { const reason = /<Message>(.*)<\/Message>/gi.exec(decoded); message = reason ? `Upload to bucket failed: ${reason[1]}` : 'Upload to bucket failed.'; } else { errorData = decoded; } } return new PubNubAPIError(message, category, status, errorData); } /** * Construct PubNub endpoint error. * * @param message - Short API call error description. * @param category - Error category. * @param statusCode - Response HTTP status code. * @param [errorData] - Error information. */ constructor( message: string, public readonly category: StatusCategory, public readonly statusCode: number, public readonly errorData?: Error | Payload, ) { super(message); this.name = 'PubNubAPIError'; } /** * Convert API error object to API callback status object. * * @param operation - Request operation during which error happened. * * @returns Pre-formatted API callback status object. */ public toStatus(operation: RequestOperation): Status { return { error: true, category: this.category, operation, statusCode: this.statusCode, errorData: this.errorData, // @ts-expect-error Inner helper for JSON.stringify. toJSON: function (this: Status): string { let normalizedErrorData: Payload | undefined; const errorData = this.errorData; if (errorData) { try { if (typeof errorData === 'object') { const errorObject = { ...('name' in errorData ? { name: errorData.name } : {}), ...('message' in errorData ? { message: errorData.message } : {}), ...('stack' in errorData ? { stack: errorData.stack } : {}), ...errorData, }; normalizedErrorData = JSON.parse(JSON.stringify(errorObject, PubNubAPIError.circularReplacer())); } else normalizedErrorData = errorData; } catch (_) { normalizedErrorData = { error: 'Could not serialize the error object' }; } } // Make sure to exclude `toJSON` function from the final object. const { toJSON, ...status } = this; return JSON.stringify({ ...status, errorData: normalizedErrorData }); }, }; } /** * Convert API error object to PubNub client error object. * * @param operation - Request operation during which error happened. * @param [message] - Custom error message. * * @returns Client-facing pre-formatted endpoint call error. */ public toPubNubError(operation: RequestOperation, message?: string): PubNubError { return new PubNubError(message ?? this.message, this.toStatus(operation)); } /** * Function which handles circular references in serialized JSON. * * @returns Circular reference replacer function. * * @internal */ private static circularReplacer() { const visited = new WeakSet(); return function (_: unknown, value: object | null) { if (typeof value === 'object' && value !== null) { if (visited.has(value)) return '[Circular]'; visited.add(value); } return value; }; } /** * Check whether provided `object` is an `Error` or not. * * This check is required because the error object may be tied to a different execution context (global * environment) and won't pass `instanceof Error` from the main window. * To protect against monkey-patching, the `fetch` function is taken from an invisible `iframe` and, as a result, * it is bind to the separate execution context. Errors generated by `fetch` won't pass the simple * `instanceof Error` test. * * @param object - Object which should be checked. * * @returns `true` if `object` looks like an `Error` object. * * @internal */ private static isErrorObject(object: unknown): object is Error { if (!object || typeof object !== 'object') return false; if (object instanceof Error) return true; if ( 'name' in object && 'message' in object && typeof object.name === 'string' && typeof object.message === 'string' ) { return true; } return Object.prototype.toString.call(object) === '[object Error]'; } }