| |
| |
| |
| |
|
|
| import * as vscode from 'vscode' |
| import { AWSError } from 'aws-sdk' |
| import { ServiceException } from '@smithy/smithy-client' |
| import { isThrottlingError, isTransientError } from '@smithy/service-error-classification' |
| import { Result } from './telemetry/telemetry' |
| import { CancellationError } from './utilities/timeoutUtils' |
| import { hasKey, isNonNullable } from './utilities/tsUtils' |
| import type * as nodefs from 'fs' |
| import type * as os from 'os' |
| import { CodeWhispererStreamingServiceException } from '@amzn/codewhisperer-streaming' |
| import { driveLetterRegex } from './utilities/pathUtils' |
| import { getLogger } from './logger/logger' |
| import { crashMonitoringDirName, uploadCodeError } from './constants' |
| import { RequestCancelledError } from './request' |
|
|
| let _username = 'unknown-user' |
| let _isAutomation = false |
|
|
| |
| export function init(username: string, isAutomation: boolean) { |
| _username = username |
| _isAutomation = isAutomation |
| } |
|
|
| export const errorCode = { |
| invalidConnection: 'InvalidConnection', |
| } |
|
|
| export interface ErrorInformation { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| readonly name?: string |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| readonly code?: string |
|
|
| |
| |
| |
| |
| |
| readonly cause?: Error |
|
|
| |
| |
| |
| |
| |
| |
| readonly details?: Record<string, unknown> |
|
|
| |
| |
| |
| readonly cancelled?: boolean |
|
|
| |
| |
| |
| |
| |
| |
| readonly documentationUri?: vscode.Uri |
| } |
|
|
| export class UnknownError extends Error { |
| public override readonly name = 'UnknownError' |
|
|
| public constructor(public readonly cause: unknown) { |
| super(String(cause)) |
| } |
|
|
| public static cast(obj: unknown): Error { |
| return obj instanceof Error ? obj : new UnknownError(obj) |
| } |
| } |
|
|
| |
| |
| |
| export interface NamedErrorConstructor { |
| |
| |
| |
| new (message: string, info?: Omit<ErrorInformation, 'name'>): ToolkitError |
|
|
| |
| |
| |
| chain<T extends this>( |
| this: T, |
| error: unknown, |
| message: string, |
| info?: Omit<ErrorInformation, 'name' | 'cause'> |
| ): InstanceType<T> |
| } |
|
|
| |
| |
| |
| export class ToolkitError extends Error implements ErrorInformation { |
| |
| |
| |
| |
| public override readonly message: string |
| public readonly code: string | undefined |
| public readonly details: Record<string, unknown> | undefined |
|
|
| |
| |
| |
| |
| |
| readonly #cause: Error | undefined |
| readonly #name: string |
| readonly #documentationUri: any |
| readonly #cancelled: boolean | undefined |
|
|
| public constructor(message: string, info: ErrorInformation = {}) { |
| super(message) |
| this.message = message |
| this.code = info.code |
| this.details = info.details |
| this.#cause = info.cause |
| this.#name = info.name ?? super.name |
| this.#cancelled = info.cancelled |
| this.#documentationUri = info.documentationUri |
| } |
|
|
| |
| |
| |
| public get cause(): Error | undefined { |
| return this.#cause |
| } |
|
|
| |
| |
| |
| public override get name(): string { |
| return this.#name |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public get cancelled(): boolean { |
| return this.#cancelled ?? isUserCancelledError(this.cause) |
| } |
|
|
| |
| |
| |
| public get documentationUri(): vscode.Uri | undefined { |
| return this.#documentationUri |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| public get trace(): string { |
| const message = formatError(this) |
|
|
| if (!this.cause) { |
| return message |
| } |
|
|
| |
| const residual = this.cause instanceof ToolkitError ? this.cause.trace : formatError(this.cause) |
|
|
| return `${message}\n\t -> ${residual}` |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public static chain(error: unknown, message: string, info?: Omit<ErrorInformation, 'cause'>): ToolkitError { |
| return new this(message, { |
| ...info, |
| cause: UnknownError.cast(error), |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| public static named(name: string): NamedErrorConstructor { |
| return class extends ToolkitError { |
| public override get name() { |
| return name |
| } |
|
|
| |
| |
| public static override chain< |
| T extends new (...args: ConstructorParameters<NamedErrorConstructor>) => ToolkitError, |
| >(this: T, ...args: Parameters<NamedErrorConstructor['chain']>): InstanceType<T> { |
| return ToolkitError.chain.call(this, ...args) as InstanceType<T> |
| } |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function getErrorMsg(err: Error | undefined, withCause: boolean = false): string | undefined { |
| if (err === undefined) { |
| return undefined |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const anyDesc = (err as any).error_description |
| const errDesc = typeof anyDesc === 'string' ? anyDesc.trim() : '' |
| let msg = errDesc !== '' ? errDesc : err.message?.trim() |
|
|
| if (typeof msg !== 'string') { |
| return undefined |
| } |
|
|
| |
| if (withCause) { |
| const errorId = getErrorId(err) |
| |
| |
| if (errorId && errorId !== 'Error') { |
| msg = `${errorId}: ${msg}` |
| } |
|
|
| const cause = (err as any).cause |
| return `${msg}${cause ? ' | ' + getErrorMsg(cause, withCause) : ''}` |
| } |
|
|
| return msg |
| } |
|
|
| export function formatError(err: Error): string { |
| const code = hasCode(err) && err.code !== err.name ? `[${err.code}]` : undefined |
| const parts = [`${err.name}:`, getErrorMsg(err), code, formatDetails(err)] |
|
|
| return parts.filter(isNonNullable).join(' ') |
| } |
|
|
| function formatDetails(err: Error): string | undefined { |
| const details: Record<string, string | undefined> = {} |
|
|
| if (err instanceof ToolkitError && err.details !== undefined) { |
| for (const [k, v] of Object.entries(err.details)) { |
| details[k] = String(v) |
| } |
| } else if (isAwsError(err)) { |
| details['statusCode'] = String(err.statusCode ?? '') |
| details['requestId'] = getRequestId(err) |
| details['extendedRequestId'] = err.extendedRequestId |
| } |
|
|
| if (Object.keys(details).length === 0) { |
| return |
| } |
|
|
| const joined = Object.entries(details) |
| .filter(([_, v]) => !!v) |
| .map(([k, v]) => `${k}: ${v}`) |
| .join('; ') |
|
|
| return `(${joined})` |
| } |
|
|
| export function getTelemetryResult(error: unknown | undefined): Result { |
| if (error === undefined) { |
| return 'Succeeded' |
| } else if (isUserCancelledError(error)) { |
| return 'Cancelled' |
| } |
|
|
| return 'Failed' |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function scrubNames(s: string, username?: string) { |
| let r = '' |
| const fileExtRe = /\.[^.\/]+$/ |
| const slashdot = /^[~.]*[\/\\]*/ |
|
|
| |
| const keep = new Set<string>([ |
| '~', |
| '.', |
| '..', |
| '.aws', |
| 'aws', |
| 'sso', |
| 'cache', |
| 'credentials', |
| 'config', |
| 'Users', |
| 'users', |
| 'home', |
| 'tmp', |
| 'aws-toolkit-vscode', |
| 'globalStorage', |
| crashMonitoringDirName, |
| ]) |
|
|
| if (username && username.length > 2) { |
| s = s.replaceAll(username, 'x') |
| } |
|
|
| |
| |
| s = s.replace(/(profile)\s*[:'"]?\s*([\w-]+)['"']?/gi, '$1 [REDACTED]') |
|
|
| |
| s = s.replace(/\s+/g, ' ') |
|
|
| |
| |
| const words = s.split(/\s+/) |
| for (const word of words) { |
| const pathSegments = word.split(/[\/\\]/) |
| if (pathSegments.length < 2) { |
| |
| r += ' ' + word |
| continue |
| } |
|
|
| |
| |
| let scrubbed = '' |
| |
| const start = word.trimStart().match(slashdot)?.[0] ?? '' |
| pathSegments[0] = pathSegments[0].trimStart().replace(slashdot, '') |
| for (const seg of pathSegments) { |
| if (driveLetterRegex.test(seg)) { |
| scrubbed += seg |
| } else if (keep.has(seg)) { |
| scrubbed += '/' + seg |
| } else { |
| |
| const nonAscii = seg.match(/[^\p{ASCII}]/u)?.[0] ?? '' |
| |
| const ascii = seg.replace(/[^$[\](){}:;'" ]+/g, 'x') |
| scrubbed += `/${ascii}${nonAscii}` |
| } |
| } |
|
|
| |
| const fileExt = pathSegments[pathSegments.length - 1].match(fileExtRe) ?? '' |
| r += ` ${start.replace(/\\/g, '/')}${scrubbed.replace(/^[\/\\]+/, '')}${fileExt}` |
| } |
|
|
| return r.trim() |
| } |
|
|
| |
| |
| |
| |
| |
| export function getTelemetryReasonDesc(err: unknown | undefined): string | undefined { |
| const m = typeof err === 'string' ? err : (getErrorMsg(err as Error, true) ?? '') |
| const msg = scrubNames(m, _username) |
|
|
| |
| return msg && msg.length > 0 ? msg.substring(0, 350) : undefined |
| } |
|
|
| export function getTelemetryReason(error: unknown | undefined): string | undefined { |
| |
| |
| |
|
|
| if (error === undefined) { |
| return undefined |
| } else if (error instanceof CancellationError) { |
| return error.agent |
| } else if (error instanceof ToolkitError) { |
| |
| return getTelemetryReason(error.cause) ?? error.code ?? error.name |
| } else if (error instanceof Error) { |
| return (error as { code?: string }).code ?? error.name |
| } |
|
|
| return 'Unknown' |
| } |
|
|
| |
| |
| |
| |
| |
| export function resolveErrorMessageToDisplay(error: unknown, defaultMessage: string): string { |
| const mainMsg = error instanceof ToolkitError ? error.message : defaultMessage |
| |
| const bestErr = error ? findBestErrorInChain(error as Error) : undefined |
| const bestMsg = getErrorMsg(bestErr) |
| return bestMsg && bestMsg !== mainMsg ? `${mainMsg}: ${bestMsg}` : mainMsg |
| } |
|
|
| |
| |
| |
| const _preferredErrors: RegExp[] = [ |
| /^ConflictException$/, |
| /^ValidationException$/, |
| /^ResourceNotFoundException$/, |
| /^ServiceQuotaExceededException$/, |
| /^AccessDeniedException$/, |
| /^InvalidPermissions$/, |
| /^EPIPE$/, |
| /^EPERM$/, |
| ] |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function findBestErrorInChain(error: Error, preferredErrors = _preferredErrors): Error | undefined { |
| |
| |
| let bestErr: Error & { code?: string; cause?: Error; error_description?: string } = error |
| let err: typeof bestErr | undefined |
|
|
| for (let i = 0; i < 100; i++) { |
| err = i === 0 ? bestErr.cause : err?.cause |
| if (!err) { |
| break |
| } |
|
|
| |
| |
| const errCode = err.code?.trim() ?? '' |
| const prefer = |
| (errCode !== '' && preferredErrors.some((re) => re.test(errCode))) || |
| |
| isFilesystemError(err) || |
| isPermissionsError(err) |
|
|
| if (isAwsError(err) || (prefer && !isAwsError(bestErr))) { |
| if (isAwsError(err) && !isAwsError(bestErr)) { |
| bestErr = err |
| continue |
| } |
|
|
| const errDesc = err.error_description |
| if (typeof errDesc === 'string' && errDesc.trim() !== '') { |
| bestErr = err |
| continue |
| } |
|
|
| if (!bestErr.error_description && prefer) { |
| bestErr = err |
| } |
| } |
| } |
|
|
| return bestErr |
| } |
|
|
| export function isCodeWhispererStreamingServiceException( |
| error: unknown |
| ): error is CodeWhispererStreamingServiceException { |
| if (error === undefined) { |
| return false |
| } |
|
|
| return error instanceof Error && hasFault(error) && hasMetadata(error) && hasName(error) |
| } |
|
|
| function hasFault<T>(error: T): error is T & { $fault: 'client' | 'server' } { |
| const fault = (error as { $fault?: unknown }).$fault |
| return typeof fault === 'string' && (fault === 'client' || fault === 'server') |
| } |
|
|
| function hasMetadata<T>(error: T): error is T & Pick<CodeWhispererStreamingServiceException, '$metadata'> { |
| return typeof (error as { $metadata?: unknown })?.$metadata === 'object' |
| } |
|
|
| function hasResponse<T>(error: T): error is T & Pick<ServiceException, '$response'> { |
| return typeof (error as { $response?: unknown })?.$response === 'object' |
| } |
|
|
| function hasName<T>(error: T): error is T & { name: string } { |
| return typeof (error as { name?: unknown })?.name === 'string' |
| } |
|
|
| |
| export function isAwsError(error: unknown): error is AWSError & { error_description?: string } { |
| if (error === undefined) { |
| return false |
| } |
|
|
| return error instanceof Error && hasCode(error) && hasTime(error) |
| } |
|
|
| export function isServiceException(error: unknown): error is ServiceException { |
| return error instanceof ServiceException |
| } |
|
|
| export function hasCode<T>(error: T): error is T & { code: string } { |
| return typeof (error as { code?: unknown }).code === 'string' |
| } |
|
|
| |
| |
| |
| |
| |
| export function getErrorId(error: Error): string { |
| |
| return hasCode(error) ? error.code : error.name |
| } |
|
|
| function hasTime(error: Error): error is typeof error & { time: Date } { |
| return (error as { time?: unknown }).time instanceof Date |
| } |
|
|
| export function isUserCancelledError(error: unknown): boolean { |
| return ( |
| CancellationError.isUserCancelled(error) || |
| (error instanceof ToolkitError && error.cancelled) || |
| error instanceof RequestCancelledError |
| ) |
| } |
|
|
| |
| |
| |
| export function isClientFault(error: ServiceException): boolean { |
| return error.$fault === 'client' && !(isThrottlingError(error) || isTransientError(error)) |
| } |
|
|
| export function getRequestId(err: unknown): string | undefined { |
| |
| |
| if (typeof (err as any)?.$metadata?.requestId === 'string') { |
| return (err as any).$metadata.requestId |
| } |
|
|
| if (isAwsError(err)) { |
| return err.requestId |
| } |
| } |
|
|
| export function getHttpStatusCode(err: unknown): number | undefined { |
| if (hasResponse(err) && err?.$response?.statusCode !== undefined) { |
| return err?.$response?.statusCode |
| } |
| if (hasMetadata(err) && err.$metadata?.httpStatusCode !== undefined) { |
| return err.$metadata?.httpStatusCode |
| } |
|
|
| return undefined |
| } |
|
|
| export function isFilesystemError(err: unknown): boolean { |
| if ( |
| err instanceof vscode.FileSystemError || |
| (hasCode(err) && |
| (err.code === 'EEXIST' || |
| err.code === 'EISDIR' || |
| err.code === 'ENOTDIR' || |
| err.code === 'EMFILE' || |
| err.code === 'ENOENT' || |
| err.code === 'ENOTEMPTY')) |
| ) { |
| return true |
| } |
|
|
| return false |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export function isFileNotFoundError(err: unknown): boolean { |
| if (err instanceof vscode.FileSystemError) { |
| return err.code === vscode.FileSystemError.FileNotFound().code |
| } else if (hasCode(err)) { |
| return err.code === 'ENOENT' || err.code === 'FileNotFound' |
| } |
|
|
| return false |
| } |
|
|
| export function isPermissionsError(err: unknown): boolean { |
| if (err instanceof vscode.FileSystemError) { |
| return ( |
| err.code === vscode.FileSystemError.NoPermissions().code || |
| |
| (err.code === 'Unknown' && err.message.includes('EACCES: permission denied')) |
| ) |
| } else if (hasCode(err)) { |
| |
| |
| return err.code === 'EACCES' |
| } |
|
|
| return false |
| } |
|
|
| function modeToString(mode: number) { |
| return Array.from('rwxrwxrwx') |
| .map((c, i, a) => ((mode >> (a.length - (i + 1))) & 1 ? c : '-')) |
| .join('') |
| } |
|
|
| function vscodeModeToString(mode: vscode.FileStat['permissions']) { |
| |
| if (mode === undefined) { |
| return 'rwx------' |
| } else if (mode === vscode.FilePermission.Readonly) { |
| return 'r-x------' |
| } |
|
|
| |
| if (_isAutomation) { |
| throw new Error('vscode.FileStat.permissions gained new fields, update this logic') |
| } |
| } |
|
|
| function getEffectivePerms(uid: number, gid: number, stats: nodefs.Stats) { |
| const mode = stats.mode |
| const isOwner = uid === stats.uid |
| const isGroup = gid === stats.gid && !isOwner |
|
|
| |
| |
| |
| |
| if (!isOwner && !isGroup) { |
| return { |
| isAmbiguous: true, |
| effective: mode & 0o007 & ((mode & 0o070) >> 3), |
| } |
| } |
|
|
| const ownerMask = isOwner ? 0o700 : 0 |
| const groupMask = isGroup ? 0o070 : 0 |
|
|
| return { |
| isAmbiguous: false, |
| effective: ((mode & groupMask) >> 3) | ((mode & ownerMask) >> 6), |
| } |
| } |
|
|
| |
| |
| export type PermissionsTriplet = `${'r' | '-' | '*'}${'w' | '-' | '*'}${'x' | '-' | '*'}` |
| export class PermissionsError extends ToolkitError { |
| public readonly actual: string |
|
|
| static fromNodeFileStats(stats: nodefs.Stats, userInfo: os.UserInfo<string>) { |
| const mode = `${stats.isDirectory() ? 'd' : '-'}${modeToString(stats.mode)}` |
| const owner = stats.uid === userInfo.uid ? (stats.uid === -1 ? '' : userInfo.username) : String(stats.uid) |
| const group = String(stats.gid) |
| const { effective, isAmbiguous } = getEffectivePerms(userInfo.uid, userInfo.gid, stats) |
| const actual = modeToString(effective).slice(-3) |
| const isOwner = stats.uid === -1 ? 'unknown' : userInfo.uid === stats.uid |
|
|
| return { mode, owner, group, actual, isAmbiguous, isOwner } |
| } |
|
|
| static fromVscodeFileStats(stats: vscode.FileStat, userInfo: os.UserInfo<string>) { |
| const isDir = !!(stats.type & vscode.FileType.Directory) |
| const mode = `${isDir ? 'd' : '-'}${vscodeModeToString(stats.permissions)}` |
| const owner = '' |
| const group = '' |
| const isAmbiguous = true |
| const actual = mode |
| const isOwner = 'unknown' |
|
|
| return { mode, owner, group, actual, isAmbiguous, isOwner } |
| } |
|
|
| |
| |
| |
| |
| |
| public constructor( |
| public readonly uri: vscode.Uri, |
| public readonly stats: nodefs.Stats | vscode.FileStat, |
| public readonly userInfo: os.UserInfo<string>, |
| public readonly expected: PermissionsTriplet, |
| source?: unknown |
| ) { |
| const o = (stats as any).type |
| ? PermissionsError.fromVscodeFileStats(stats as vscode.FileStat, userInfo) |
| : PermissionsError.fromNodeFileStats(stats as nodefs.Stats, userInfo) |
|
|
| const resolvedExpected = Array.from(expected) |
| .map((c, i) => (c === '*' ? o.actual[i] : c)) |
| .join('') |
| const actualText = !o.isAmbiguous ? o.actual : `${o.mode.slice(-6, -3)} & ${o.mode.slice(-3)} (ambiguous)` |
|
|
| |
| |
| |
| if (o.actual === resolvedExpected && !o.isAmbiguous && source !== undefined) { |
| throw source |
| } |
|
|
| super(`${uri.fsPath} has incorrect permissions. Expected ${resolvedExpected}, found ${actualText}.`, { |
| code: 'InvalidPermissions', |
| details: { |
| isOwner: o.isOwner, |
| mode: `${o.mode}${o.owner === '' ? '' : ` ${o.owner}`}${o.group === '' ? '' : ` ${o.group}`}`, |
| }, |
| }) |
|
|
| this.actual = o.actual |
| } |
| } |
|
|
| |
| |
| |
| export class ClientError extends ToolkitError { |
| constructor(message: string, info: ErrorInformation = { code: '400' }) { |
| super(message, info) |
| } |
| } |
|
|
| |
| |
| |
| export class ServiceError extends ToolkitError { |
| constructor(message: string, info: ErrorInformation = { code: '500' }) { |
| super(message, info) |
| } |
| } |
|
|
| export class UploadURLExpired extends ClientError { |
| constructor() { |
| super( |
| "I’m sorry, I wasn't able to generate code. A connection timed out or became unavailable. Please try again or check the following:\n\n- Exclude non-essential files in your workspace’s .gitignore.\n\n- Check that your network connection is stable.", |
| { code: 'UploadURLExpired' } |
| ) |
| } |
| } |
|
|
| export class UploadCodeError extends ServiceError { |
| constructor(statusCode: string) { |
| super(uploadCodeError, { code: `UploadCode-${statusCode}` }) |
| } |
| } |
|
|
| export class ContentLengthError extends ClientError { |
| constructor(message: string, info: ErrorInformation = { code: 'ContentLengthError' }) { |
| super(message, info) |
| } |
| } |
|
|
| export function isNetworkError(err?: unknown): err is Error & { code: string } { |
| if (!(err instanceof Error)) { |
| return false |
| } |
|
|
| if ( |
| isVSCodeProxyError(err) || |
| isSocketTimeoutError(err) || |
| isEnoentError(err) || |
| isEaccesError(err) || |
| isEbadfError(err) || |
| isEconnRefusedError(err) || |
| err instanceof AwsClientResponseError || |
| isBadResponseCode(err) || |
| isEbusyError(err) |
| ) { |
| return true |
| } |
|
|
| if (!hasCode(err)) { |
| return false |
| } |
|
|
| return [ |
| 'ENOTFOUND', |
| 'EAI_AGAIN', |
| 'ECONNRESET', |
| 'ECONNREFUSED', |
| 'ETIMEDOUT', |
| 'ENETUNREACH', |
| 'ERR_TLS_CERT_ALTNAME_INVALID', |
| 'EPROTO', |
| 'EHOSTUNREACH', |
| 'EADDRINUSE', |
| 'ENOBUFS', |
| 'EADDRNOTAVAIL', |
| 'SELF_SIGNED_CERT_IN_CHAIN', |
| 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', |
| 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', |
| 'HPE_INVALID_VERSION', |
| 'DEPTH_ZERO_SELF_SIGNED_CERT', |
| 'ENOTCONN', |
| 'ENETDOWN', |
| 'ECONNABORTED', |
| 'CERT_HAS_EXPIRED', |
| 'EAI_FAIL', |
| '502', |
| 'InternalServerException', |
| 'ERR_SSL_WRONG_VERSION_NUMBER', |
| ].includes(err.code) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function isVSCodeProxyError(err: Error): boolean { |
| return isError(err, 'Error', 'Failed to establish a socket connection to proxies') |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function isSocketTimeoutError(err: Error): boolean { |
| return isError(err, 'TimeoutError', 'Connection timed out after') |
| } |
|
|
| |
| |
| |
| |
| function isEnoentError(err: Error): boolean { |
| return isError(err, 'ENOENT', 'getaddrinfo ENOENT') |
| } |
|
|
| function isEaccesError(err: Error): boolean { |
| return isError(err, 'EACCES', 'connect EACCES') |
| } |
|
|
| function isEbadfError(err: Error): boolean { |
| return isError(err, 'EBADF', 'connect EBADF') |
| } |
|
|
| function isEconnRefusedError(err: Error): boolean { |
| return isError(err, 'Error', 'connect ECONNREFUSED') |
| } |
|
|
| function isEbusyError(err: Error) { |
| |
| return isError(err, 'EBUSY', 'getaddrinfo EBUSY') |
| } |
|
|
| |
| export function isError(err: Error, id: string, messageIncludes: string = '') { |
| |
| |
| return (err.name === id || (err as any).code === id) && err.message.includes(messageIncludes) |
| } |
|
|
| |
| |
| |
| |
| const errorResponseCodes = [302, 403, 404, 502, 503] |
|
|
| |
| |
| |
| function isBadResponseCode(error: Error) { |
| if (isNaN(Number(error.name))) { |
| return |
| } |
| const statusCode = parseInt(error.name, 10) |
| return errorResponseCodes.includes(statusCode) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export class AwsClientResponseError extends Error { |
| |
| protected constructor(err: unknown) { |
| const underlyingErrorMsg = AwsClientResponseError.tryExtractReasonFromSyntaxError(err) |
|
|
| |
| |
| |
| |
| |
| |
| if (!(underlyingErrorMsg && err instanceof Error)) { |
| throw Error(`Cannot create AwsClientResponseError from ${JSON.stringify(err)}}`) |
| } |
|
|
| super(underlyingErrorMsg) |
| } |
|
|
| |
| |
| |
| |
| static instanceIf<T>(err: T): AwsClientResponseError | T { |
| const reason = AwsClientResponseError.tryExtractReasonFromSyntaxError(err) |
| if (reason) { |
| getLogger().debug(`Creating AwsClientResponseError from SyntaxError: %O`, err) |
| return new AwsClientResponseError(err) |
| } |
| return err |
| } |
|
|
| |
| |
| |
| |
| static tryExtractReasonFromSyntaxError(err: unknown): string | undefined { |
| if ( |
| !( |
| err instanceof SyntaxError && |
| err.message.includes('inspect the hidden field {error}.$response on this object') |
| ) |
| ) { |
| return undefined |
| } |
|
|
| |
| if (hasKey(err, '$response') && err['$response'] !== undefined) { |
| const response = err['$response'] |
| if (response) { |
| if (hasKey(response, 'reason') && response['reason'] !== undefined) { |
| return response['reason'] as string |
| } else { |
| |
| |
| return `No 'reason' field in '$response' | ${JSON.stringify(response)} | ${err.message}` |
| } |
| } |
| } else { |
| |
| |
| return `No '$response' field in SyntaxError | ${err.message}` |
| } |
|
|
| return undefined |
| } |
| } |
|
|
| |
| |
| |
| export function tryRun<T>(fn: () => T, shouldThrow: (err: Error) => boolean, logMsg?: string): T | undefined |
| export function tryRun<T>( |
| fn: () => Promise<T>, |
| shouldThrow: (err: Error) => boolean, |
| logMsg?: string |
| ): Promise<T> | undefined |
| export function tryRun<T>( |
| fn: () => T | Promise<T>, |
| shouldThrow: (err: Error) => boolean, |
| logMsg?: string |
| ): T | Promise<T | void> | undefined { |
| |
|
|
| const catchErr = (err: Error) => { |
| if (shouldThrow(err)) { |
| throw err |
| } |
|
|
| getLogger().error(logMsg ?? 'unknown caller: Error ignored: %s', err) |
| } |
|
|
| try { |
| const result = fn() |
| if (result instanceof Promise) { |
| return result.catch(catchErr) |
| } |
| return result |
| } catch (error: any) { |
| catchErr(error) |
| } |
| } |
|
|