Skip to content

Commit 43e24a4

Browse files
fix(codecatalyst): dev env error messages not being displayed
Problem: The following issue was started due to a change needed for Velox, but this will be able to apply to all AWS services. When certain exceptions are thrown by AWS services we want them to be displayed directly to the user in a vscode window. Currently we only display Toolkit errors to the user, otherwise only logging the error to the logs. There are some AWS errors that we want to show to the user, but on top of that AWS errors can be wrapped inside a Toolkit error. Solution: When deciding which error message to show to users: - Show the exact error message if it is an AWS error and is in our defined list of 'prioritizied' errors. - If we get a Toolkit error, check if it wraps a prioritized error, extract it, then display that. Signed-off-by: Nikolas Komonen <[email protected]>
1 parent 7c4c6b6 commit 43e24a4

File tree

4 files changed

+171
-6
lines changed

4 files changed

+171
-6
lines changed

src/extension.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ import { join } from 'path'
6363
import { Experiments, Settings } from './shared/settings'
6464
import { getCodeCatalystDevEnvId, isReleaseVersion } from './shared/vscode/env'
6565
import { Commands, registerErrorHandler } from './shared/vscode/commands2'
66-
import { isUserCancelledError, ToolkitError } from './shared/errors'
66+
import { isUserCancelledError, resolveErrorMessageToDisplay } from './shared/errors'
6767
import { Logging } from './shared/logger/commands'
6868
import { UriHandler } from './shared/vscode/uriHandler'
6969
import { telemetry } from './shared/telemetry/telemetry'
@@ -273,7 +273,7 @@ async function handleError(error: unknown, topic: string, defaultMessage: string
273273

274274
const logsItem = localize('AWS.generic.message.viewLogs', 'View Logs...')
275275
const logId = getLogger().error(`${topic}: %s`, error)
276-
const message = error instanceof ToolkitError ? error.message : defaultMessage
276+
const message = resolveErrorMessageToDisplay(error, defaultMessage)
277277

278278
await vscode.window.showErrorMessage(message, logsItem).then(async resp => {
279279
if (resp === logsItem) {

src/shared/clients/codecatalystClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ class CodeCatalystClientInternal {
306306
return this.call(this.sdkClient.createAccessToken(args), false)
307307
} catch (e) {
308308
if ((e as Error).name === 'ServiceQuotaExceededException') {
309-
throw new ToolkitError('Access token limit exceeded', { code: 'ServiceQuotaExceeded' })
309+
throw new ToolkitError('Access token limit exceeded', { cause: e as Error })
310310
}
311311
throw e
312312
}

src/shared/errors.ts

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,18 +275,104 @@ export function getTelemetryReason(error: unknown | undefined): string | undefin
275275
return 'Unknown'
276276
}
277277

278-
export function isAwsError(error: unknown | undefined): error is AWSError {
278+
/**
279+
* Determines the appropriate error message to display to the user.
280+
*
281+
* We do not want to display every error message to the user, this
282+
* resolves what we actually want to show them based off the given
283+
* input.
284+
*/
285+
export function resolveErrorMessageToDisplay(error: unknown, defaultMessage: string): string {
286+
const mainMessage = error instanceof ToolkitError ? error.message : defaultMessage
287+
// We want to explicitly show certain AWS Error messages if they are raised
288+
const prioritizedMessage = findPrioritizedAwsError(error)?.message
289+
return prioritizedMessage ? `${mainMessage}: ${prioritizedMessage}` : mainMessage
290+
}
291+
292+
/**
293+
* Patterns that match the value of {@link AWSError.code}
294+
*/
295+
export const prioritizedAwsErrors: RegExp[] = [
296+
/^ConflictException$/,
297+
/^ValidationException$/,
298+
/^ResourceNotFoundException$/,
299+
/^ServiceQuotaExceededException$/,
300+
]
301+
302+
/**
303+
* Sometimes there are AWS specific errors that we want to explicitly
304+
* show to the user, these are 'prioritized' errors.
305+
*
306+
* In certain cases we may unknowingly wrap these errors in a Toolkit error
307+
* as the 'cause', in return masking the the underlying error from being
308+
* reported to the user.
309+
*
310+
* Since we do not want developers to worry if they are allowed to wrap
311+
* a specific AWS error in a Toolkit error, we will instead handle
312+
* it in this function by extracting the 'prioritized' error if it is
313+
* found.
314+
*
315+
* @returns new ToolkitError with prioritized error message, otherwise original error
316+
*/
317+
export function findPrioritizedAwsError(
318+
error: unknown,
319+
prioritizedErrors = prioritizedAwsErrors
320+
): AWSError | undefined {
321+
const awsError = findAwsErrorInCausalChain(error)
322+
323+
if (awsError === undefined || !prioritizedErrors.some(regex => regex.test(awsError.code))) {
324+
return undefined
325+
}
326+
327+
return awsError
328+
}
329+
330+
/**
331+
* This will search through the causal chain of errors (if it exists)
332+
* until it finds an {@link AWSError}.
333+
*
334+
* {@link ToolkitError} instances can wrap a 'cause', which is the underlying
335+
* error that caused it.
336+
*
337+
* @returns AWSError if found, otherwise undefined
338+
*/
339+
export function findAwsErrorInCausalChain(error: unknown): AWSError | undefined {
340+
let currentError = error
341+
342+
while (currentError !== undefined) {
343+
if (isAwsError(currentError)) {
344+
return currentError
345+
}
346+
347+
// TODO: Base Error has 'cause' in ES2022. If we upgrade this can be made
348+
// non-ToolkitError specific
349+
if (currentError instanceof ToolkitError && currentError.cause !== undefined) {
350+
currentError = currentError.cause
351+
continue
352+
}
353+
354+
return undefined
355+
}
356+
357+
return undefined
358+
}
359+
360+
export function isAwsError(error: unknown): error is AWSError {
279361
if (error === undefined) {
280362
return false
281363
}
282364

283-
return error instanceof Error && hasCode(error) && (error as { time?: unknown }).time instanceof Date
365+
return error instanceof Error && hasCode(error) && hasTime(error)
284366
}
285367

286368
function hasCode(error: Error): error is typeof error & { code: string } {
287369
return typeof (error as { code?: unknown }).code === 'string'
288370
}
289371

372+
function hasTime(error: Error): error is typeof error & { time: Date } {
373+
return (error as { time?: unknown }).time instanceof Date
374+
}
375+
290376
export function isUserCancelledError(error: unknown): boolean {
291377
return CancellationError.isUserCancelled(error) || (error instanceof ToolkitError && error.cancelled)
292378
}

src/test/shared/errors.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
*/
55

66
import * as assert from 'assert'
7-
import { getTelemetryReason, getTelemetryResult, ToolkitError } from '../../shared/errors'
7+
import { getTelemetryReason, getTelemetryResult, resolveErrorMessageToDisplay, ToolkitError } from '../../shared/errors'
88
import { CancellationError } from '../../shared/utilities/timeoutUtils'
99
import { UnauthorizedException } from '@aws-sdk/client-sso'
10+
import { AWSError } from 'aws-sdk'
1011

1112
describe('ToolkitError', function () {
1213
it('can store an error message', function () {
@@ -219,3 +220,81 @@ describe('Telemetry', function () {
219220
})
220221
})
221222
})
223+
224+
describe('resolveErrorMessageToDisplay()', function () {
225+
const defaultMessage = 'My Default Message!'
226+
const normalErrorMessage = 'Normal error message'
227+
const toolkitErrorMessage = 'Toolkit error message'
228+
const awsErrorMessage = 'AWS error message'
229+
230+
it('returns default message if no error is given', function () {
231+
const message = resolveErrorMessageToDisplay(undefined, defaultMessage)
232+
assert.strictEqual(message, defaultMessage)
233+
})
234+
235+
it('returns default message if normal error is given', function () {
236+
const message = resolveErrorMessageToDisplay(new Error(normalErrorMessage), defaultMessage)
237+
assert.strictEqual(message, defaultMessage)
238+
})
239+
240+
it('returns toolkit message if toolkit error is given', function () {
241+
const message = resolveErrorMessageToDisplay(new ToolkitError(toolkitErrorMessage), defaultMessage)
242+
assert.strictEqual(message, toolkitErrorMessage)
243+
})
244+
245+
describe('prioritized AWS errors', function () {
246+
class TestAwsError extends Error implements AWSError {
247+
constructor(readonly code: string, message: string, readonly time: Date) {
248+
super(message)
249+
}
250+
}
251+
252+
const errorTime: Date = new Date()
253+
const prioritizedAwsErrorNames: string[] = [
254+
'ServiceQuotaExceededException',
255+
'ConflictException',
256+
'ValidationException',
257+
'ResourceNotFoundException',
258+
]
259+
const prioritiziedAwsErrors: TestAwsError[] = prioritizedAwsErrorNames.map(name => {
260+
return new TestAwsError(name, awsErrorMessage, errorTime)
261+
})
262+
263+
// Sanity check specific errors are resolved as expected
264+
prioritiziedAwsErrors.forEach(error => {
265+
it(`resolves ${error.code} message when provided directly`, function () {
266+
const message = resolveErrorMessageToDisplay(error, defaultMessage)
267+
assert.strictEqual(message, `${defaultMessage}: ${awsErrorMessage}`)
268+
})
269+
})
270+
271+
it('resolves AWS Error when nested in a ToolkitError cause', function () {
272+
const awsError = prioritiziedAwsErrors[0]
273+
const toolkitError = new ToolkitError(toolkitErrorMessage, { cause: awsError })
274+
275+
const message = resolveErrorMessageToDisplay(toolkitError, defaultMessage)
276+
277+
assert.strictEqual(message, `${toolkitErrorMessage}: ${awsErrorMessage}`)
278+
})
279+
280+
it('resolves AWS Error when nested multiple levels in a ToolkitError cause', function () {
281+
const awsError = prioritiziedAwsErrors[0]
282+
const toolkitErrorTail = new ToolkitError(`${toolkitErrorMessage}-tail`, { cause: awsError })
283+
const toolkitErrorMiddle = new ToolkitError(`${toolkitErrorMessage}-middle`, { cause: toolkitErrorTail })
284+
const toolkitErrorHead = new ToolkitError(`${toolkitErrorMessage}-head`, { cause: toolkitErrorMiddle })
285+
286+
const message = resolveErrorMessageToDisplay(toolkitErrorHead, defaultMessage)
287+
288+
assert.strictEqual(message, `${toolkitErrorMessage}-head: ${awsErrorMessage}`)
289+
})
290+
291+
it('resolves toolkit message if cause is non-prioritized AWS error', function () {
292+
const nonPrioritizedAwsError = new TestAwsError('NonPrioritizedAwsException', awsErrorMessage, errorTime)
293+
const toolkitError = new ToolkitError(toolkitErrorMessage, { cause: nonPrioritizedAwsError })
294+
295+
const message = resolveErrorMessageToDisplay(toolkitError, defaultMessage)
296+
297+
assert.strictEqual(message, toolkitErrorMessage)
298+
})
299+
})
300+
})

0 commit comments

Comments
 (0)