Skip to content

Commit 03301e8

Browse files
anivarosama-rizkbobbor
authored
fix(api-graphql): trigger WebSocket reconnection on auth errors (#14569)
Detect authentication errors (UnauthorizedException, NotAuthorizedException, Token expired) in subscription error responses using exact errorType matching and close the WebSocket to trigger reconnection with fresh tokens. Handles both EVENT_SUBSCRIBE_ERROR (structured errors array) and GQL_ERROR (payload errors) message types. The socket close fires the existing CONNECTION_CLOSED → ConnectionDisrupted → ReconnectionMonitor flow, so no changes needed in DataStore's disconnectionHandler. Fixes #12954 Co-authored-by: Osama Rizk <41443120+osama-rizk@users.noreply.github.com> Co-authored-by: Philipp Andreas Paul <phandpau@amazon.de>
1 parent b4c0f94 commit 03301e8

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aws-amplify/api-graphql': patch
3+
---
4+
5+
fix(api-graphql): trigger WebSocket reconnection on auth errors to restore subscriptions after token expiration

packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,17 +701,50 @@ export abstract class AWSWebSocketProvider {
701701
});
702702

703703
let errorMessage = JSON.stringify(payload ?? data);
704+
let isAuthError = false;
705+
706+
const AUTH_ERROR_TYPES = [
707+
'UnauthorizedException',
708+
'Unauthorized',
709+
'NotAuthorizedException',
710+
];
704711

705712
if (type === MESSAGE_TYPES.EVENT_SUBSCRIBE_ERROR) {
706713
const { errors } = JSON.parse(String(message.data));
707714
if (Array.isArray(errors) && errors.length > 0) {
708715
const error = errors[0];
709716
errorMessage = `${error.errorType}: ${error.message}`;
717+
isAuthError =
718+
AUTH_ERROR_TYPES.includes(error.errorType) ||
719+
error.message?.includes('Token expired');
720+
}
721+
} else if (
722+
type === MESSAGE_TYPES.GQL_ERROR &&
723+
payload &&
724+
typeof payload === 'object'
725+
) {
726+
const { errors } = payload as Record<string, unknown>;
727+
if (Array.isArray(errors) && errors.length > 0) {
728+
const error = errors[0] as Record<string, string>;
729+
isAuthError =
730+
AUTH_ERROR_TYPES.includes(error.errorType) ||
731+
error.message?.includes('Token expired');
710732
}
711733
}
712734

713735
this.logger.debug(`${CONTROL_MSG.CONNECTION_FAILED}: ${errorMessage}`);
714736

737+
// On auth errors, close the socket to trigger reconnection with fresh tokens.
738+
// The onclose handler fires _errorDisconnect → CONNECTION_CLOSED, which the
739+
// ConnectionStateMonitor picks up as ConnectionDisrupted and the
740+
// ReconnectionMonitor reconnects with refreshed credentials.
741+
if (isAuthError && this.awsRealTimeSocket) {
742+
this.logger.warn(
743+
'Subscription failed due to auth error, closing WebSocket to trigger reconnection with fresh tokens',
744+
);
745+
this.awsRealTimeSocket.close(1000, 'Auth error - reconnecting');
746+
}
747+
715748
observer.error({
716749
errors: [
717750
{

0 commit comments

Comments
 (0)