Skip to content

Commit bd69954

Browse files
Merge pull request #3302 from nkomonen-amazon/awsErrorMessage
fix(codecatalyst): dev env error messages not being displayed
2 parents eb38055 + 43e24a4 commit bd69954

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)