Skip to content

Commit ab829fd

Browse files
authored
Merge master into feature/separate-sessions
2 parents 659e292 + 590c2fa commit ab829fd

File tree

3 files changed

+274
-160
lines changed

3 files changed

+274
-160
lines changed

packages/core/src/shared/errors.ts

Lines changed: 104 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
220267
export 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-
264299
export 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. */
275310
export 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

310317
export 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
*/
336341
export 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
/^ConflictException$/,
348354
/^ValidationException$/,
349355
/^ResourceNotFoundException$/,
@@ -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

412409
export 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

462460
export 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

474472
export function isFileNotFoundError(err: unknown): boolean {

packages/core/src/shared/utilities/logAndShowUtils.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,12 @@ import { Logging } from '../logger/commands'
1414
* Logs the error. Then determines what kind of error message should be shown, if
1515
* at all.
1616
*
17+
* TODO: Currently only used for errors from commands and webview. Use in more places (explorer,
18+
* nodes, ...). Must be guaranteed to initialize prior to every other Toolkit component.
19+
*
1720
* @param error The error itself
1821
* @param topic The prefix of the error message
1922
* @param defaultMessage The message to show if once cannot be resolved from the given error
20-
*
21-
* SIDE NOTE:
22-
* This is only being used for errors from commands and webview, there's plenty of other places
23-
* (explorer, nodes, ...) where it could be used. It needs to be apart of some sort of `core`
24-
* module that is guaranteed to initialize prior to every other Toolkit component.
25-
* Logging and telemetry would fit well within this core module.
2623
*/
2724
export async function logAndShowError(
2825
localize: nls.LocalizeFunc,

0 commit comments

Comments
 (0)