Skip to content

Commit 56507e2

Browse files
fix(auth): SyntaxError not being considered Network Error (aws#5512)
## Problem: There were some previous changes that caused the existing implementation of a `SyntaxError` to not be considered a Network Error. And during token refresh we were seeing SyntaxErrors still appearing ## Solution: Create a single class which wraps a `SyntaxError`, called `AwsClientResponseError`. Under the hood of a SyntaxError is the real error that originates from a failed AWS SDK Client response, this is how we determine if it should be an `AwsClientResponseError`. This new class can only be created if given the correct SyntaxError instance to `AwsClientResponseError.instanceIf()`. Then we can simply check if an error matches by using `instanceif AwsClientResponseError` Any existing code that was related to this scenario has all been centralized in to the AwsClientResponseError class. --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Signed-off-by: Nikolas Komonen <[email protected]>
1 parent 7e948c5 commit 56507e2

File tree

6 files changed

+99
-50
lines changed

6 files changed

+99
-50
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "Auth: `SyntaxError` causing unexpected SSO logout"
4+
}

packages/core/src/auth/sso/clients.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { pageableToCollection, partialClone } from '../../shared/utilities/colle
2828
import { assertHasProps, isNonNullable, RequiredProps, selectFrom } from '../../shared/utilities/tsUtils'
2929
import { getLogger } from '../../shared/logger'
3030
import { SsoAccessTokenProvider } from './ssoAccessTokenProvider'
31-
import { ToolkitError, getHttpStatusCode, getReasonFromSyntaxError, isClientFault } from '../../shared/errors'
31+
import { AwsClientResponseError, isClientFault } from '../../shared/errors'
3232
import { DevSettings } from '../../shared/settings'
3333
import { SdkError } from '@aws-sdk/types'
3434
import { HttpRequest, HttpResponse } from '@smithy/protocol-http'
@@ -90,20 +90,8 @@ export class OidcClient {
9090
try {
9191
response = await this.client.createToken(request as CreateTokenRequest)
9292
} catch (err) {
93-
// In rare cases the SDK client may get unexpected data from its API call.
94-
// This will throw a SyntaxError that contains the returned data.
95-
if (err instanceof SyntaxError) {
96-
getLogger().error(`createToken: SSOIDC Client received an unexpected non-JSON response: %O`, err)
97-
throw new ToolkitError(
98-
`SDK Client unexpected error response: data response code: ${getHttpStatusCode(err)}, data reason: ${getReasonFromSyntaxError(err)}`,
99-
{
100-
code: `${getHttpStatusCode(err)}`,
101-
cause: err,
102-
}
103-
)
104-
} else {
105-
throw err
106-
}
93+
const newError = AwsClientResponseError.instanceIf(err)
94+
throw newError
10795
}
10896
assertHasProps(response, 'accessToken', 'expiresIn')
10997

packages/core/src/codewhispererChat/controllers/chat/controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import { LspController } from '../../../amazonq/lsp/lspController'
4848
import { CodeWhispererSettings } from '../../../codewhisperer/util/codewhispererSettings'
4949
import { getSelectedCustomization } from '../../../codewhisperer/util/customizationUtil'
5050
import { FeatureConfigProvider } from '../../../shared/featureConfig'
51-
import { getHttpStatusCode, getReasonFromSyntaxError } from '../../../shared/errors'
51+
import { getHttpStatusCode, AwsClientResponseError } from '../../../shared/errors'
5252

5353
export interface ChatControllerMessagePublishers {
5454
readonly processPromptChatMessage: MessagePublisher<PromptMessage>
@@ -299,7 +299,7 @@ export class ChatController {
299299
errorMessage = e.toUpperCase()
300300
} else if (e instanceof SyntaxError) {
301301
// Workaround to handle case when LB returns web-page with error and our client doesn't return proper exception
302-
errorMessage = getReasonFromSyntaxError(e) ?? defaultMessage
302+
errorMessage = AwsClientResponseError.tryExtractReasonFromSyntaxError(e) ?? defaultMessage
303303
} else if (e instanceof CodeWhispererStreamingServiceException) {
304304
errorMessage = e.message
305305
requestID = e.$metadata.requestId

packages/core/src/shared/errors.ts

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -789,11 +789,11 @@ export function isNetworkError(err?: unknown): err is Error & { code: string } {
789789
if (
790790
isVSCodeProxyError(err) ||
791791
isSocketTimeoutError(err) ||
792-
isHttpSyntaxError(err) ||
793792
isEnoentError(err) ||
794793
isEaccesError(err) ||
795794
isEbadfError(err) ||
796-
isEconnRefusedError(err)
795+
isEconnRefusedError(err) ||
796+
err instanceof AwsClientResponseError
797797
) {
798798
return true
799799
}
@@ -847,18 +847,6 @@ function isSocketTimeoutError(err: Error): boolean {
847847
return isError(err, 'TimeoutError', 'Connection timed out after')
848848
}
849849

850-
/**
851-
* Expected JSON response from HTTP request, but got an error HTML error page instead.
852-
*
853-
* IMPORTANT:
854-
*
855-
* This function is influenced by {@link getReasonFromSyntaxError()} since it modifies the error
856-
* message with the real underlying reason, instead of the default "Unexpected token" message.
857-
*/
858-
function isHttpSyntaxError(err: Error): boolean {
859-
return isError(err, 'SyntaxError', 'SDK Client unexpected error response')
860-
}
861-
862850
/**
863851
* We were seeing errors of ENOENT for the oidc FQDN (eg: oidc.us-east-1.amazonaws.com) during the SSO flow.
864852
* Our assumption is that this is an intermittent error.
@@ -887,30 +875,69 @@ function isError(err: Error, id: string, messageIncludes: string = '') {
887875
}
888876

889877
/**
890-
* AWS SDK clients may rarely make requests that results in something other than JSON data
891-
* being returned (e.g html). This will cause the client to throw a SyntaxError as a result
878+
* AWS SDK clients make requests with the expected result to be JSON data.
879+
* But in some cases the request may fail and result in an error HTML page being returned instead
880+
* of the JSON. This will cause the client to throw a `SyntaxError` as a result
892881
* of attempt to deserialize the non-JSON data.
893-
* While the contents of the response may contain sensitive information, there may be a reason
894-
* for failure embedded. This function attempts to extract that reason.
895882
*
896-
* Example error message before extracting:
897-
* "Unexpected token '<', "<html><bod"... is not valid JSON Deserialization error: to see the raw response, inspect the hidden field {error}.$response on this object."
883+
* But within the `SyntaxError` instance is the real reason for the failure.
884+
* This class attempts to extract the underlying issue from the SyntaxError.
898885
*
899-
* If the reason cannot be found or the error is not a SyntaxError, return undefined.
886+
* Example SyntaxError message before extracting the underlying issue:
887+
* - "Unexpected token '<', "<html><bod"... is not valid JSON Deserialization error: to see the raw response, inspect the hidden field {error}.$response on this object."
888+
* Once we extract the real error message from the hidden field, `$response.reason`, we get messages similar to:
889+
* - "SDK Client unexpected error response: data response code: 403, data reason: Forbidden | Unexpected ..."
900890
*/
901-
export function getReasonFromSyntaxError(err: Error): string | undefined {
902-
if (!(err instanceof SyntaxError)) {
903-
return undefined
891+
export class AwsClientResponseError extends Error {
892+
/** Use {@link instanceIf} to create instance. */
893+
protected constructor(err: unknown) {
894+
const underlyingErrorMsg = AwsClientResponseError.tryExtractReasonFromSyntaxError(err)
895+
896+
/**
897+
* This condition should never be hit since {@link AwsClientResponseError.instanceIf}
898+
* is the only way to create an instance of this class, due to the constructor not being public.
899+
*
900+
* The following only exists to make the type checker happy.
901+
*/
902+
if (!(underlyingErrorMsg && err instanceof Error)) {
903+
throw Error(`Cannot create AwsClientResponseError from ${JSON.stringify(err)}}`)
904+
}
905+
906+
super(underlyingErrorMsg)
904907
}
905908

906-
if (hasKey(err, '$response')) {
907-
const response = err['$response']
908-
if (response && hasKey(response, 'reason')) {
909-
return response['reason'] as string
909+
/**
910+
* Resolves an instance of {@link AwsClientResponseError} if the given error matches certain criteria.
911+
* Otherwise the original error is returned.
912+
*/
913+
static instanceIf<T>(err: T): AwsClientResponseError | T {
914+
const reason = AwsClientResponseError.tryExtractReasonFromSyntaxError(err)
915+
if (reason && reason.startsWith('SDK Client unexpected error response: data response code:')) {
916+
getLogger().debug(`Creating AwsClientResponseError from SyntaxError: %O`, err)
917+
return new AwsClientResponseError(err)
910918
}
919+
return err
911920
}
912921

913-
return undefined
922+
/**
923+
* Returns the true underlying error message from a `SyntaxError`, if possible.
924+
* Otherwise returning undefined.
925+
*/
926+
static tryExtractReasonFromSyntaxError(err: unknown): string | undefined {
927+
if (!(err instanceof SyntaxError)) {
928+
return undefined
929+
}
930+
931+
// See the class docstring to explain how we know the existence of the following keys
932+
if (hasKey(err, '$response')) {
933+
const response = err['$response']
934+
if (response && hasKey(response, 'reason')) {
935+
return response['reason'] as string
936+
}
937+
}
938+
939+
return undefined
940+
}
914941
}
915942

916943
/**

packages/core/src/test/shared/errors.test.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
isNetworkError,
1616
resolveErrorMessageToDisplay,
1717
scrubNames,
18+
AwsClientResponseError,
1819
ToolkitError,
1920
tryRun,
2021
UnknownError,
@@ -482,6 +483,13 @@ describe('util', function () {
482483
assert.deepStrictEqual(getTelemetryReasonDesc(toolkitError), 'ToolkitError Message | Cause Message x/x/x/x.txt')
483484
})
484485

486+
function makeSyntaxErrorWithSdkClientError() {
487+
const syntaxError: Error = new SyntaxError('The SyntaxError message')
488+
// Under the hood of a SyntaxError may be a hidden field with the real reason for the failure
489+
;(syntaxError as any)['$response'] = { reason: 'SDK Client unexpected error response: data response code: 500' }
490+
return syntaxError
491+
}
492+
485493
it('isNetworkError()', function () {
486494
assert.deepStrictEqual(
487495
isNetworkError(new Error('Failed to establish a socket connection to proxies BLAH BLAH BLAH')),
@@ -493,15 +501,33 @@ describe('util', function () {
493501
false,
494502
'Incorrectly indicated as network error'
495503
)
496-
let err = new Error('SDK Client unexpected error response: Blah Blah Blah')
497-
err.name = 'SyntaxError'
498-
assert.deepStrictEqual(isNetworkError(err), true, 'Did not indicate SyntaxError as network error')
499504

500-
err = new Error('getaddrinfo ENOENT oidc.us-east-1.amazonaws.com')
505+
const awsClientResponseError = AwsClientResponseError.instanceIf(makeSyntaxErrorWithSdkClientError())
506+
assert.deepStrictEqual(
507+
isNetworkError(awsClientResponseError),
508+
true,
509+
'Did not indicate SyntaxError as network error'
510+
)
511+
512+
const err = new Error('getaddrinfo ENOENT oidc.us-east-1.amazonaws.com')
501513
;(err as any).code = 'ENOENT'
502514
assert.deepStrictEqual(isNetworkError(err), true, 'Did not indicate ENOENT error as network error')
503515
})
504516

517+
it('AwsClientResponseError', function () {
518+
const syntaxError = makeSyntaxErrorWithSdkClientError()
519+
520+
assert.deepStrictEqual(
521+
AwsClientResponseError.tryExtractReasonFromSyntaxError(syntaxError),
522+
'SDK Client unexpected error response: data response code: 500'
523+
)
524+
const responseError = AwsClientResponseError.instanceIf(syntaxError)
525+
assert(!(responseError instanceof SyntaxError))
526+
assert(responseError instanceof Error)
527+
assert(responseError instanceof AwsClientResponseError)
528+
assert(responseError.message === 'SDK Client unexpected error response: data response code: 500')
529+
})
530+
505531
it('scrubNames()', async function () {
506532
const fakeUser = 'jdoe123'
507533
assert.deepStrictEqual(scrubNames('', fakeUser), '')
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "Auth: `SyntaxError` causing unexpected SSO logout"
4+
}

0 commit comments

Comments
 (0)