@@ -87,6 +87,18 @@ export interface ErrorInformation {
8787 readonly documentationUri ?: vscode . Uri
8888}
8989
90+ export class UnknownError extends Error {
91+ public override readonly name = 'UnknownError'
92+
93+ public constructor ( public readonly cause : unknown ) {
94+ super ( String ( cause ) )
95+ }
96+
97+ public static cast ( obj : unknown ) : Error {
98+ return obj instanceof Error ? obj : new UnknownError ( obj )
99+ }
100+ }
101+
90102/**
91103 * Anonymous class with a pre-defined error name.
92104 */
@@ -217,9 +229,44 @@ export class ToolkitError extends Error implements ErrorInformation {
217229 }
218230}
219231
232+ export function getErrorMsg ( err : Error | undefined ) : string | undefined {
233+ if ( err === undefined ) {
234+ return undefined
235+ }
236+
237+ // error_description is a non-standard SDK field added by (at least) OIDC service.
238+ // If present, it has better information, so prefer it to `message`.
239+ // https://github.com/aws/aws-toolkit-jetbrains/commit/cc9ed87fa9391dd39ac05cbf99b4437112fa3d10
240+ //
241+ // Example:
242+ //
243+ // [error] API response (oidc.us-east-1.amazonaws.com /token): {
244+ // name: 'InvalidGrantException',
245+ // '$fault': 'client',
246+ // '$metadata': {
247+ // httpStatusCode: 400,
248+ // requestId: '7f5af448-5af7-45f2-8e47-5808deaea4ab',
249+ // extendedRequestId: undefined,
250+ // cfId: undefined
251+ // },
252+ // error: 'invalid_grant',
253+ // error_description: 'Invalid refresh token provided',
254+ // message: 'UnknownError'
255+ // }
256+ const anyDesc = ( err as any ) . error_description
257+ const errDesc = typeof anyDesc === 'string' ? anyDesc . trim ( ) : ''
258+ const msg = errDesc !== '' ? errDesc : err . message ?. trim ( )
259+
260+ if ( typeof msg !== 'string' ) {
261+ return undefined
262+ }
263+
264+ return msg
265+ }
266+
220267export function formatError ( err : Error ) : string {
221268 const code = hasCode ( err ) && err . code !== err . name ? `[${ err . code } ]` : undefined
222- const parts = [ `${ err . name } :` , err . message , code , formatDetails ( err ) ]
269+ const parts = [ `${ err . name } :` , getErrorMsg ( err ) , code , formatDetails ( err ) ]
223270
224271 return parts . filter ( isNonNullable ) . join ( ' ' )
225272}
@@ -233,7 +280,7 @@ function formatDetails(err: Error): string | undefined {
233280 }
234281 } else if ( isAwsError ( err ) ) {
235282 details [ 'statusCode' ] = String ( err . statusCode ?? '' )
236- details [ 'requestId' ] = err . requestId
283+ details [ 'requestId' ] = getRequestId ( err )
237284 details [ 'extendedRequestId' ] = err . extendedRequestId
238285 }
239286
@@ -249,18 +296,6 @@ function formatDetails(err: Error): string | undefined {
249296 return `(${ joined } )`
250297}
251298
252- export class UnknownError extends Error {
253- public override readonly name = 'UnknownError'
254-
255- public constructor ( public readonly cause : unknown ) {
256- super ( String ( cause ) )
257- }
258-
259- public static cast ( obj : unknown ) : Error {
260- return obj instanceof Error ? obj : new UnknownError ( obj )
261- }
262- }
263-
264299export function getTelemetryResult ( error : unknown | undefined ) : Result {
265300 if ( error === undefined ) {
266301 return 'Succeeded'
@@ -273,38 +308,10 @@ export function getTelemetryResult(error: unknown | undefined): Result {
273308
274309/** Gets the (partial) error message detail for the `reasonDesc` field. */
275310export function getTelemetryReasonDesc ( err : unknown | undefined ) : string | undefined {
276- if ( err === undefined ) {
277- return undefined
278- }
279-
280- const e = err as any
281- // error_description is a non-standard SDK field added by OIDC service.
282- // It has improved messages, so prefer it if found.
283- // https://github.com/aws/aws-toolkit-jetbrains/commit/cc9ed87fa9391dd39ac05cbf99b4437112fa3d10
284- //
285- // Example:
286- //
287- // [error] API response (oidc.us-east-1.amazonaws.com /token): {
288- // name: 'InvalidGrantException',
289- // '$fault': 'client',
290- // '$metadata': {
291- // httpStatusCode: 400,
292- // requestId: '7f5af448-5af7-45f2-8e47-5808deaea4ab',
293- // extendedRequestId: undefined,
294- // cfId: undefined
295- // },
296- // error: 'invalid_grant',
297- // error_description: 'Invalid refresh token provided',
298- // message: 'UnknownError'
299- // }
300- const msg = e ?. error_description ?. trim ( ) ? e ?. error_description ?. trim ( ) : e ?. message ?. trim ( )
301-
302- if ( typeof msg === 'string' ) {
303- // Truncate to 200 chars.
304- return msg !== '' ? msg . substring ( 0 , 200 ) : undefined
305- }
311+ const msg = getErrorMsg ( err as Error )
306312
307- return undefined
313+ // Truncate to 200 chars.
314+ return msg && msg . length > 0 ? msg . substring ( 0 , 200 ) : undefined
308315}
309316
310317export function getTelemetryReason ( error : unknown | undefined ) : string | undefined {
@@ -317,7 +324,7 @@ export function getTelemetryReason(error: unknown | undefined): string | undefin
317324 } else if ( error instanceof CancellationError ) {
318325 return error . agent
319326 } else if ( error instanceof ToolkitError ) {
320- // TODO: prefer the error.error field if present? (see comment in `getTelemetryReasonDesc `)
327+ // TODO: prefer the error.error field if present? (see comment in `getErrorMsg `)
321328 return getTelemetryReason ( error . cause ) ?? error . code ?? error . name
322329 } else if ( error instanceof Error ) {
323330 return ( error as { code ?: string } ) . code ?? error . name
@@ -327,23 +334,22 @@ export function getTelemetryReason(error: unknown | undefined): string | undefin
327334}
328335
329336/**
330- * Determines the appropriate error message to display to the user.
337+ * Tries to build the most intuitive/relevant message to show to the user.
331338 *
332- * We do not want to display every error message to the user, this
333- * resolves what we actually want to show them based off the given
334- * input.
339+ * User can see the full error chain in the logs Output channel.
335340 */
336341export function resolveErrorMessageToDisplay ( error : unknown , defaultMessage : string ) : string {
337- const mainMessage = error instanceof ToolkitError ? error . message : defaultMessage
338- // We want to explicitly show certain AWS Error messages if they are raised
339- const prioritizedMessage = findPrioritizedAwsError ( error ) ?. message
340- return prioritizedMessage ? `${ mainMessage } : ${ prioritizedMessage } ` : mainMessage
342+ const mainMsg = error instanceof ToolkitError ? error . message : defaultMessage
343+ // Try to find the most useful/relevant error in the `cause` chain.
344+ const bestErr = error ? findBestErrorInChain ( error as Error ) : undefined
345+ const bestMsg = getErrorMsg ( bestErr )
346+ return bestMsg && bestMsg !== mainMsg ? `${ mainMsg } : ${ bestMsg } ` : mainMsg
341347}
342348
343349/**
344350 * Patterns that match the value of {@link AWSError.code}
345351 */
346- export const prioritizedAwsErrors : RegExp [ ] = [
352+ const _preferredErrors : RegExp [ ] = [
347353 / ^ C o n f l i c t E x c e p t i o n $ / ,
348354 / ^ V a l i d a t i o n E x c e p t i o n $ / ,
349355 / ^ R e s o u r c e N o t F o u n d E x c e p t i o n $ / ,
@@ -352,61 +358,52 @@ export const prioritizedAwsErrors: RegExp[] = [
352358]
353359
354360/**
355- * Sometimes there are AWS specific errors that we want to explicitly
356- * show to the user, these are 'prioritized' errors .
361+ * Searches the `cause` chain (if any) for the most useful/relevant { @link AWSError} to surface to
362+ * the user, preferring "deeper" errors (lower-level, closer to the root cause) when all else is equal .
357363 *
358- * In certain cases we may unknowingly wrap these errors in a Toolkit error
359- * as the 'cause', in return masking the the underlying error from being
360- * reported to the user.
364+ * These conditions determine precedence (in order):
365+ * - required: AWSError type
366+ * - `error_description` field
367+ * - `code` matches one of `preferredErrors`
368+ * - cause chain depth (the deepest error wins)
361369 *
362- * Since we do not want developers to worry if they are allowed to wrap
363- * a specific AWS error in a Toolkit error, we will instead handle
364- * it in this function by extracting the 'prioritized' error if it is
365- * found.
370+ * @param error Error whose `cause` chain will be searched.
371+ * @param preferredErrors Error `code` field must match one of these, else it is discarded. Pass `[/./]` to match any AWSError.
366372 *
367- * @returns new ToolkitError with prioritized error message, otherwise original error
373+ * @returns Best match, or ` error` if a better match is not found.
368374 */
369- export function findPrioritizedAwsError (
370- error : unknown ,
371- prioritizedErrors = prioritizedAwsErrors
372- ) : AWSError | undefined {
373- const awsError = findAwsErrorInCausalChain ( error )
374-
375- if ( awsError === undefined || ! prioritizedErrors . some ( regex => regex . test ( awsError . code ) ) ) {
376- return undefined
377- }
378-
379- return awsError
380- }
381-
382- /**
383- * This will search through the causal chain of errors (if it exists)
384- * until it finds an {@link AWSError}.
385- *
386- * {@link ToolkitError} instances can wrap a 'cause', which is the underlying
387- * error that caused it.
388- *
389- * @returns AWSError if found, otherwise undefined
390- */
391- export function findAwsErrorInCausalChain ( error : unknown ) : AWSError | undefined {
392- let currentError = error
375+ export function findBestErrorInChain ( error : Error , preferredErrors = _preferredErrors ) : Error | undefined {
376+ // TODO: Base Error has 'cause' in ES2022. So does our own `ToolkitError`.
377+ // eslint-disable-next-line @typescript-eslint/naming-convention
378+ let bestErr : Error & { cause ?: Error ; error_description ?: string } = error
379+ let err : typeof bestErr | undefined
380+
381+ for ( let i = 0 ; ( err || i === 0 ) && i < 100 ; i ++ ) {
382+ err = i === 0 ? bestErr . cause : err ?. cause
383+
384+ if ( isAwsError ( err ) ) {
385+ if ( ! isAwsError ( bestErr ) ) {
386+ bestErr = err // Prefer AWSError.
387+ continue
388+ }
393389
394- while ( currentError !== undefined ) {
395- if ( isAwsError ( currentError ) ) {
396- return currentError
397- }
390+ const errDesc = err . error_description
391+ if ( typeof errDesc === 'string' && errDesc . trim ( ) !== '' ) {
392+ bestErr = err // Prefer (deepest) error with error_description.
393+ continue
394+ }
398395
399- // TODO: Base Error has 'cause' in ES2022. If we upgrade this can be made
400- // non-ToolkitError specific
401- if ( currentError instanceof ToolkitError && currentError . cause !== undefined ) {
402- currentError = currentError . cause
403- continue
396+ // const bestErrCode = bestErr.code?.trim() ?? ''
397+ // const bestErrMatches = bestErrCode !== '' && preferredErrors.some(re => re.test(bestErrCode))
398+ const errCode = err . code ?. trim ( ) ?? ''
399+ const errMatches = errCode !== '' && preferredErrors . some ( re => re . test ( errCode ) )
400+ if ( ! bestErr . error_description && errMatches ) {
401+ bestErr = err
402+ }
404403 }
405-
406- return undefined
407404 }
408405
409- return undefined
406+ return bestErr
410407}
411408
412409export function isCodeWhispererStreamingServiceException (
@@ -432,7 +429,8 @@ function hasName<T>(error: T): error is T & { name: string } {
432429 return typeof ( error as { name ?: unknown } ) . name === 'string'
433430}
434431
435- export function isAwsError ( error : unknown ) : error is AWSError {
432+ // eslint-disable-next-line @typescript-eslint/naming-convention
433+ export function isAwsError ( error : unknown ) : error is AWSError & { error_description ?: string } {
436434 if ( error === undefined ) {
437435 return false
438436 }
@@ -460,15 +458,15 @@ export function isClientFault(error: ServiceException): boolean {
460458}
461459
462460export function getRequestId ( err : unknown ) : string | undefined {
463- if ( isAwsError ( err ) ) {
464- return err . requestId
465- }
466-
467461 // XXX: Checking `err instanceof ServiceException` fails for `SSOOIDCServiceException` even
468462 // though it subclasses @aws -sdk/smithy-client.ServiceException
469463 if ( typeof ( err as any ) ?. $metadata ?. requestId === 'string' ) {
470464 return ( err as any ) . $metadata . requestId
471465 }
466+
467+ if ( isAwsError ( err ) ) {
468+ return err . requestId
469+ }
472470}
473471
474472export function isFileNotFoundError ( err : unknown ) : boolean {
0 commit comments