/*! * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0 */ 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' // eslint-disable-line no-restricted-imports 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 /** Performs one-time initialization, to avoid circular dependencies. */ export function init(username: string, isAutomation: boolean) { _username = username _isAutomation = isAutomation } export const errorCode = { invalidConnection: 'InvalidConnection', } export interface ErrorInformation { /** * Error names are optional, but if provided they should be generic yet self-explanatory. * * A name does not need to precisely describe why something failed and instead should focus * on the overarching theme or module that caused the problem. For example, VS Code uses * `FileSystemError` for a diverse range of problems relating to their file system API. * * The choice of granularity for an error name largely comes down to what other errors are * possible; names are meant to immediately disambiguate from the potentially thousands of * other causes for a failure. If there is any doubt that a certain name might be confusing, * opt for something more specific and verbose. */ readonly name?: string /** * An error code is used to describe specific failure modes within entire classes of errors. * * The exact value used here is free-form but should be kept stable whenever possible. * Value stability is especially important if any error handling logic directly relies on * error codes. Prefer using {@link ToolkitError.named} and narrowing this class if any * substantial logic is needed for handling specific error codes. * * As an example, one might choose to describe SSO error codes for extra type-safety: * ```ts * enum CreateTokenErrorCode { * SlowDown = 'SlowDownException', * ExpiredToken = 'ExpiredTokenException', * UnauthorizedClient = 'UnauthorizedClientException', * AuthorizationPending = 'AuthorizationPendingException', * } * * class CreateTokenError extends ToolkitError.named('CreateTokenError') { * public constructor(public readonly code: CreateTokenErrorCode) { * super('Failed to create SSO access token', { code }) * } * } * ``` */ readonly code?: string /** * Used to describe errors with a direct cause-effect relationship. * * See {@link code} for creating descriptive errors without a chainable root cause. */ readonly cause?: Error /** * Structured information that may be used for logging. * * Consumers should strictly use this field for observability purposes as producers can * put whatever they want here. */ readonly details?: Record /** * Flag to determine if the error was from a user-initiated cancellation. */ readonly cancelled?: boolean /** * A link to documentation relevant to this error. * * TODO: use this throughout the codebase. * TODO: prefer `Error.error_uri` if present (from OIDC/OAuth service)? */ 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) } } /** * Anonymous class with a pre-defined error name. */ export interface NamedErrorConstructor { /** * See {@link ToolkitError} */ new (message: string, info?: Omit): ToolkitError /** * See {@link ToolkitError.chain} */ chain( this: T, error: unknown, message: string, info?: Omit ): InstanceType } /** * Generic error class for handling exceptions within the Toolkit. */ export class ToolkitError extends Error implements ErrorInformation { /** * A message that could potentially be shown to the user. This should not contain any * sensitive information and should be limited in technical detail. */ public override readonly message: string public readonly code: string | undefined public readonly details: Record | undefined /** * We guard against mutation to stop a developer from creating a circular chain of errors. * The alternative is to truncate errors to an arbitrary depth though that doesn't address * why the error chain is deep. */ 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 } /** * The original error that caused this error (if any). */ public get cause(): Error | undefined { return this.#cause } /** * The name of the error. This is not necessarily the same as the class name. */ public override get name(): string { return this.#name } /** * See {@link ErrorInformation.cancelled cancelled}. * * Whether or not an error is considered 'cancelled' is determined either by explicit * assignment on construction or by finding a 'cancelled' error within its causal chain. */ public get cancelled(): boolean { return this.#cancelled ?? isUserCancelledError(this.cause) } /** * The associated documentation, if it exists. Otherwise undefined. */ public get documentationUri(): vscode.Uri | undefined { return this.#documentationUri } /** * A formatted string that is analogous to a stack trace. While stack traces enumerate every * call site, this trace enumerates every throw site. * * The motivation here is that stack traces are often not very useful to anyone but the original * developers. This is especially true for JavaScript where source maps are needed to parse traces * from bundled applications. We want a trace that is informative but not excessively noisy. */ public get trace(): string { const message = formatError(this) if (!this.cause) { return message } // Stack overflows are only possible if `cause` is changed after instantiation const residual = this.cause instanceof ToolkitError ? this.cause.trace : formatError(this.cause) return `${message}\n\t -> ${residual}` } /** * Creates a new {@link ToolkitError} instance that was directly caused by another error. * * @param error - The original error that caused this error. * @param message - A descriptive message for the new error. * @param info - Additional information about the error. * @returns {ToolkitError} The new ToolkitError instance. * * @recommendation It is recommended to throw the returned ToolkitError instance instead of just returning it. * This way, the error can be properly propagated and handled in the calling code. * * Example: * ```typescript * try { * // Some code that might throw an error * } catch (error) { * throw ToolkitError.chain(error, 'An error occurred during operation', { operation: 'someOperation' }); * } * ``` */ public static chain(error: unknown, message: string, info?: Omit): ToolkitError { return new this(message, { ...info, cause: UnknownError.cast(error), }) } /** * Creates a new {@link ToolkitError} _class_ with a constant name. * * Constructor (class) names are not preserved when bundling due to name mangling. Extending off * this new anonymous class will ensure that the specified name is always present. */ public static named(name: string): NamedErrorConstructor { return class extends ToolkitError { public override get name() { return name } // TypeScript does not allow the use of `this` types for generic prototype methods unfortunately // This implementation is equivalent to re-assignment i.e. an unbound method on the prototype public static override chain< T extends new (...args: ConstructorParameters) => ToolkitError, >(this: T, ...args: Parameters): InstanceType { return ToolkitError.chain.call(this, ...args) as InstanceType } } } } /** * Derives an error message from the given error object. * Depending on the Error, the property used to derive the message can vary. * * @param withCause Append the message(s) from the cause chain, recursively. * The message(s) are delimited by ' | '. Eg: msg1 | causeMsg1 | causeMsg2 */ export function getErrorMsg(err: Error | undefined, withCause: boolean = false): string | undefined { if (err === undefined) { return undefined } // Non-standard SDK fields added by the OIDC service, to conform to the OAuth spec // (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1) : // - error: code per the OAuth spec // - error_description: improved error message provided by OIDC service. Prefer this to // `message` if present. // https://github.com/aws/aws-toolkit-jetbrains/commit/cc9ed87fa9391dd39ac05cbf99b4437112fa3d10 // - error_uri: not provided by OIDC currently? // // Example: // // [error] API response (oidc.us-east-1.amazonaws.com /token): { // name: 'InvalidGrantException', // '$fault': 'client', // '$metadata': { // httpStatusCode: 400, // requestId: '7f5af448-5af7-45f2-8e47-5808deaea4ab', // extendedRequestId: undefined, // cfId: undefined // }, // error: 'invalid_grant', // error_description: 'Invalid refresh token provided', // message: 'UnknownError' // } 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 } // append the cause's message if (withCause) { const errorId = getErrorId(err) // - prepend id to message // - If a generic error does not have the `name` field explicitly set, it returns a generic 'Error' name. So skip since it is useless. 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 = {} 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' } /** * Removes potential PII from a string, for logging/telemetry. * * Examples: * - "Failed to save c:/fooß/bar/baz.txt" => "Failed to save c:/xß/x/x.txt" * - "EPERM for dir c:/Users/user1/.aws/sso/cache/abc123.json" => "EPERM for dir c:/Users/x/.aws/sso/cache/x.json" * - "Error with profile my-profile" => "Error with profile [REDACTED]" */ export function scrubNames(s: string, username?: string) { let r = '' const fileExtRe = /\.[^.\/]+$/ const slashdot = /^[~.]*[\/\\]*/ /** Allowlisted filepath segments. */ const keep = new Set([ '~', '.', '..', '.aws', 'aws', 'sso', 'cache', 'credentials', 'config', 'Users', 'users', 'home', 'tmp', 'aws-toolkit-vscode', 'globalStorage', // from vscode globalStorageUri crashMonitoringDirName, ]) if (username && username.length > 2) { s = s.replaceAll(username, 'x') } // Remove profile names that might appear in error messages // Matches "profile" followed by optional punctuation and the profile name s = s.replace(/(profile)\s*[:'"]?\s*([\w-]+)['"']?/gi, '$1 [REDACTED]') // Replace contiguous whitespace with 1 space. s = s.replace(/\s+/g, ' ') // 1. split on whitespace. // 2. scrub words that match username or look like filepaths. const words = s.split(/\s+/) for (const word of words) { const pathSegments = word.split(/[\/\\]/) if (pathSegments.length < 2) { // Not a filepath. r += ' ' + word continue } // Replace all (non-allowlisted) ASCII filepath segments with "x". // "/foo/bar/aws/sso/" => "/x/x/aws/sso/" let scrubbed = '' // Get the frontmatter ("/", "../", "~/", or "./"). 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 { // Save the first non-ASCII (unicode) char, if any. const nonAscii = seg.match(/[^\p{ASCII}]/u)?.[0] ?? '' // Replace all chars (except [^…]) with "x" . const ascii = seg.replace(/[^$[\](){}:;'" ]+/g, 'x') scrubbed += `/${ascii}${nonAscii}` } } // includes leading '.', eg: '.json' const fileExt = pathSegments[pathSegments.length - 1].match(fileExtRe) ?? '' r += ` ${start.replace(/\\/g, '/')}${scrubbed.replace(/^[\/\\]+/, '')}${fileExt}` } return r.trim() } /** * Gets the (partial) error message detail for the `reasonDesc` field. * * @param err Error object, or message text */ export function getTelemetryReasonDesc(err: unknown | undefined): string | undefined { const m = typeof err === 'string' ? err : (getErrorMsg(err as Error, true) ?? '') const msg = scrubNames(m, _username) // Truncate message as these strings can be very long. return msg && msg.length > 0 ? msg.substring(0, 350) : undefined } export function getTelemetryReason(error: unknown | undefined): string | undefined { // Currently the `code` field is favored over the error name even though both are useful // for describing the reason. We're only using a single `reason` field and it's just simpler // to not encode more information. if (error === undefined) { return undefined } else if (error instanceof CancellationError) { return error.agent } else if (error instanceof ToolkitError) { // TODO: prefer the error.error field if present? (see comment in `getErrorMsg`) return getTelemetryReason(error.cause) ?? error.code ?? error.name } else if (error instanceof Error) { return (error as { code?: string }).code ?? error.name } return 'Unknown' } /** * Tries to build the most intuitive/relevant message to show to the user. * * User can see the full error chain in the logs Output channel. */ export function resolveErrorMessageToDisplay(error: unknown, defaultMessage: string): string { const mainMsg = error instanceof ToolkitError ? error.message : defaultMessage // Try to find the most useful/relevant error in the `cause` chain. const bestErr = error ? findBestErrorInChain(error as Error) : undefined const bestMsg = getErrorMsg(bestErr) return bestMsg && bestMsg !== mainMsg ? `${mainMsg}: ${bestMsg}` : mainMsg } /** * Patterns that match the value of {@link AWSError.code} */ const _preferredErrors: RegExp[] = [ /^ConflictException$/, /^ValidationException$/, /^ResourceNotFoundException$/, /^ServiceQuotaExceededException$/, /^AccessDeniedException$/, /^InvalidPermissions$/, /^EPIPE$/, /^EPERM$/, ] /** * Searches the `cause` chain (if any) for the most useful/relevant {@link AWSError} to surface to * the user, preferring "deeper" errors (lower-level, closer to the root cause) when all else is equal. * * These conditions determine precedence (in order): * - is AWSError * - has `error_description` field * - has `code` matching `preferredErrors` * - is filesystem error * - cause chain depth (the deepest error wins) * * @param error Error whose `cause` chain will be searched. * @param preferredErrors Error `code` field must match one of these, else it is discarded. Pass `[/./]` to match any AWSError. * * @returns Best match, or `error` if a better match is not found. */ export function findBestErrorInChain(error: Error, preferredErrors = _preferredErrors): Error | undefined { // TODO: Base Error has 'cause' in ES2022. So does our own `ToolkitError`. // eslint-disable-next-line @typescript-eslint/naming-convention 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 bestErrCode = bestErr.code?.trim() ?? '' // const preferBest = ... const errCode = err.code?.trim() ?? '' const prefer = (errCode !== '' && preferredErrors.some((re) => re.test(errCode))) || // In priority order: isFilesystemError(err) || isPermissionsError(err) if (isAwsError(err) || (prefer && !isAwsError(bestErr))) { if (isAwsError(err) && !isAwsError(bestErr)) { bestErr = err // Prefer AWSError. continue } const errDesc = err.error_description if (typeof errDesc === 'string' && errDesc.trim() !== '') { bestErr = err // Prefer (deepest) error with error_description. 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(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(error: T): error is T & Pick { return typeof (error as { $metadata?: unknown })?.$metadata === 'object' } function hasResponse(error: T): error is T & Pick { return typeof (error as { $response?: unknown })?.$response === 'object' } function hasName(error: T): error is T & { name: string } { return typeof (error as { name?: unknown })?.name === 'string' } // eslint-disable-next-line @typescript-eslint/naming-convention 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(error: T): error is T & { code: string } { return typeof (error as { code?: unknown }).code === 'string' } /** * Returns the identifier the given error. * Depending on the implementation, the identifier may exist on a * different property. */ export function getErrorId(error: Error): string { // prioritize code over the name 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 ) } /** * Checks if the AWS SDK v3 error was caused by the client and not due to a service issue. */ export function isClientFault(error: ServiceException): boolean { return error.$fault === 'client' && !(isThrottlingError(error) || isTransientError(error)) } export function getRequestId(err: unknown): string | undefined { // XXX: Checking `err instanceof ServiceException` fails for `SSOOIDCServiceException` even // though it subclasses @aws-sdk/smithy-client.ServiceException 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 isIsDirError(err: unknown): boolean { // if (err instanceof vscode.FileSystemError) { // return err.code === vscode.FileSystemError.FileIsADirectory().code // } else if (hasCode(err)) { // return err.code === 'EISDIR' // } // return false // } // // export function isFileExistsError(err: unknown): boolean { // if (err instanceof vscode.FileSystemError) { // return err.code === vscode.FileSystemError.FileExists().code // } else if (hasCode(err)) { // return err.code === 'EEXIST' // } // 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 || // The code _should_ be `NoPermissions`, maybe this is a bug? (err.code === 'Unknown' && err.message.includes('EACCES: permission denied')) ) } else if (hasCode(err)) { // " Some operating systems report EPERM in situations unrelated to permissions." // || err.code === 'EPERM' 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']) { // XXX: vscode.FileStat.permissions only indicates "readonly" or nothing (aka "writable"). if (mode === undefined) { return 'rwx------' } else if (mode === vscode.FilePermission.Readonly) { return 'r-x------' } // XXX: future-proof in case vscode.FileStat.permissions gains more granularity. 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 // Many unix systems support multiple groups but we only know the primary // The user can still have group permissions despite not having the same `gid` // These situations are ambiguous, so the effective permissions are the // intersection of the two bitfields 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), } } // The wildcard (`*`) symbol is non-standard. It's used to represent "don't cares" and takes // on the actual flag once known. export type PermissionsTriplet = `${'r' | '-' | '*'}${'w' | '-' | '*'}${'x' | '-' | '*'}` export class PermissionsError extends ToolkitError { public readonly actual: string // This is a resolved triplet, _not_ the full bits static fromNodeFileStats(stats: nodefs.Stats, userInfo: os.UserInfo) { 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) { const isDir = !!(stats.type & vscode.FileType.Directory) const mode = `${isDir ? 'd' : '-'}${vscodeModeToString(stats.permissions)}` const owner = '' // vscode.FileStat does not currently provide file owner. const group = '' // vscode.FileStat does not currently provide file group. const isAmbiguous = true // vscode.workspace.fs.stat() is currently always ambiguous. const actual = mode const isOwner = 'unknown' // vscode.FileStat does not currently provide file owner. return { mode, owner, group, actual, isAmbiguous, isOwner } } /** * Creates a PermissionsError from a file stat() result. * * Note: pass `nodefs.Stats` when possible (in a nodejs context), because it gives much better info. */ public constructor( public readonly uri: vscode.Uri, public readonly stats: nodefs.Stats | vscode.FileStat, public readonly userInfo: os.UserInfo, 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)` // Guard against surfacing confusing error messages. If the actual perms equal the resolved // perms then odds are it wasn't really a permissions error. Some operating systems report EPERM // in situations that aren't related to permissions at all. 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 } } /** * Errors extending this class are considered "errors" in service metrics. */ export class ClientError extends ToolkitError { constructor(message: string, info: ErrorInformation = { code: '400' }) { super(message, info) } } /** * Errors extending this class are considered "faults" in service metrics. */ 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', // dns issue? 'EPROTO', // due to legacy server "unsafe legacy renegotiation"? 'EHOSTUNREACH', 'EADDRINUSE', 'ENOBUFS', // client side memory issue during http request? 'EADDRNOTAVAIL', // port not available/allowed?, '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', // This may be irrelevant as isBadResponseCode() may be all we need 'InternalServerException', 'ERR_SSL_WRONG_VERSION_NUMBER', ].includes(err.code) } /** * This error occurs on a network call if the user has set up a proxy in the * VS Code settings but the proxy is not reachable. * * Setting ID: http.proxy */ function isVSCodeProxyError(err: Error): boolean { return isError(err, 'Error', 'Failed to establish a socket connection to proxies') } /** * When making SSO OIDC calls, we were seeing errors in telemetry and narrowing it down brings us to: * https://github.com/smithy-lang/smithy-typescript/blob/6aac850af4b5b07b3941854d21e3b0158aefcacb/packages/node-http-handler/src/set-socket-timeout.ts#L7 * This looks to be thrown during http requests, so we'll consider it a network error. * * The scenario where we are actually getting the error though might actually be a bug: * https://github.com/aws/aws-sdk-js-v3/issues/6271 */ function isSocketTimeoutError(err: Error): boolean { return isError(err, 'TimeoutError', 'Connection timed out after') } /** * We were seeing errors of ENOENT for the oidc FQDN (eg: oidc.us-east-1.amazonaws.com) during the SSO flow. * Our assumption is that this is an intermittent error. */ 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) { // we were seeing errors with the message 'getaddrinfo EBUSY oidc.us-east-1.amazonaws.com' return isError(err, 'EBUSY', 'getaddrinfo EBUSY') } /** Helper function to assert given error has the expected properties */ export function isError(err: Error, id: string, messageIncludes: string = '') { // It is not always clear if the error has the expected value in the `name` or `code` field // so this checks both. return (err.name === id || (err as any).code === id) && err.message.includes(messageIncludes) } /** * These are the errors explicitly seen in telemetry. We can instead do any non-200 response code * later, but this will give us better visibility in to the actual error codes we are currently getting. */ const errorResponseCodes = [302, 403, 404, 502, 503] /** * Returns true if the given error is a bad response code */ function isBadResponseCode(error: Error) { if (isNaN(Number(error.name))) { return } const statusCode = parseInt(error.name, 10) return errorResponseCodes.includes(statusCode) } /** * AWS SDK clients make requests with the expected result to be JSON data. * But in some cases the request may fail and result in an error HTML page being returned instead * of the JSON. This will cause the client to throw a `SyntaxError` as a result * of attempt to deserialize the non-JSON data. * * But within the `SyntaxError` instance is the real reason for the failure. * This class attempts to extract the underlying issue from the SyntaxError. * * Example SyntaxError message before extracting the underlying issue: * - "Unexpected token '<', "