From 7252091438462bc968735d66ab9c4b10d0e350a3 Mon Sep 17 00:00:00 2001 From: anivar Date: Fri, 26 Sep 2025 21:20:24 +0530 Subject: [PATCH 1/2] fix(datastore): fix WebSocket reconnection after token expiration Fixes the critical issue where DataStore sync silently fails after token expiration, causing users to miss real-time updates while Hub events incorrectly report success. Resolves #12954 The `disconnectionHandler` in sync/index.ts only handled specific disconnect messages but not authentication errors. When tokens expired after ~12-24 hours: - Sync processor logged "Sync processor retry error: No current user" - WebSocket subscriptions silently failed - Hub events still reported "syncQueriesReady" - Outbound mutations worked but inbound subscriptions were dead Extended `disconnectionHandler` to detect authentication-related errors and trigger proper socket disconnection/reconnection with refreshed tokens. Added detection for authentication error messages: - "No current user" - Token expired/invalid - "Unauthorized" - Generic auth failure - "Token expired" - Explicit token expiration - "NotAuthorizedException" - Cognito auth failure When detected, the handler: 1. Logs a warning for debugging 2. Calls `datastoreConnectivity.socketDisconnected()` to force reconnection 3. Allows DataStore to re-establish subscriptions with refreshed tokens Tested with reproduction steps from issue: 1. Leave app idle for >12 hours with auth enabled 2. Wake computer/return to app 3. Verify subscriptions reconnect properly 4. Confirm real-time updates are received This ensures users don't miss critical real-time updates in production apps. --- packages/datastore/src/sync/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/datastore/src/sync/index.ts b/packages/datastore/src/sync/index.ts index 3575caab2a8..94dc7561d9f 100644 --- a/packages/datastore/src/sync/index.ts +++ b/packages/datastore/src/sync/index.ts @@ -799,6 +799,22 @@ export class SyncEngine { ) { this.datastoreConnectivity.socketDisconnected(); } + // Handle authentication errors that occur when tokens expire + // This fixes the issue where WebSocket subscriptions silently fail after token expiration + // See: https://github.com/aws-amplify/amplify-js/issues/12954 + else if ( + msg?.includes?.('No current user') || + msg?.includes?.('Unauthorized') || + msg?.includes?.('Token expired') || + msg?.includes?.('NotAuthorizedException') + ) { + logger.warn( + 'DataStore sync subscription failed due to authentication error. Triggering reconnection...', + msg, + ); + // Trigger socket disconnection to force a full reconnection with refreshed tokens + this.datastoreConnectivity.socketDisconnected(); + } }; } From e53708421d8800a3b6a488d2e9e96b27cdb7e48b Mon Sep 17 00:00:00 2001 From: anivar Date: Fri, 26 Sep 2025 22:28:55 +0530 Subject: [PATCH 2/2] fix(api-graphql): trigger WebSocket reconnection on auth errors When subscriptions receive authentication errors (token expired, unauthorized, etc.), close the WebSocket connection to force reconnection with fresh auth tokens. This ensures subscriptions resume after extended idle periods or device sleep/wake cycles. Fixes #12954 --- .../Providers/AWSWebSocketProvider/index.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts b/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts index f5738ca4475..85023f8ae2d 100644 --- a/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts +++ b/packages/api-graphql/src/Providers/AWSWebSocketProvider/index.ts @@ -701,17 +701,44 @@ export abstract class AWSWebSocketProvider { }); let errorMessage = JSON.stringify(payload ?? data); + let isAuthError = false; if (type === MESSAGE_TYPES.EVENT_SUBSCRIBE_ERROR) { const { errors } = JSON.parse(String(message.data)); if (Array.isArray(errors) && errors.length > 0) { const error = errors[0]; errorMessage = `${error.errorType}: ${error.message}`; + // Check if this is an authentication/authorization error + isAuthError = + error.errorType === 'UnauthorizedException' || + error.errorType === 'Unauthorized'; } } + // Also check for auth errors in the error message + isAuthError = + isAuthError || + errorMessage.includes('UnauthorizedException') || + errorMessage.includes('Unauthorized') || + errorMessage.includes('Token expired') || + errorMessage.includes('NotAuthorizedException') || + errorMessage.includes('401') || + errorMessage.includes('403'); + this.logger.debug(`${CONTROL_MSG.CONNECTION_FAILED}: ${errorMessage}`); + // If it's an auth error, trigger reconnection to refresh tokens + if (isAuthError && this.awsRealTimeSocket) { + this.logger.debug( + 'Subscription failed due to auth error, closing WebSocket to trigger reconnection with fresh tokens', + ); + // Close the WebSocket connection which will trigger reconnection + // The connectionStateMonitor will detect the closed connection and since + // intendedConnectionState is still 'connected', it will trigger ConnectionDisrupted + // which causes the ReconnectionMonitor to reconnect with fresh tokens + this.awsRealTimeSocket.close(1000, 'Auth error - reconnecting'); + } + observer.error({ errors: [ {