From caabcdbe10e2152dcaadf028e08c923d0fcf3b5f Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 13 Dec 2025 12:32:50 +0100 Subject: [PATCH] Tweak offline mode detection (#1404) --- .../entrypoints/popup/hooks/useVaultSync.ts | 6 +++ .../entrypoints/popup/pages/auth/Unlock.tsx | 40 ++++++++++++------- .../src/utils/WebApiService.ts | 17 +++++--- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts b/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts index 902abf924..01b6abcdc 100644 --- a/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts +++ b/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts @@ -9,6 +9,7 @@ import { useWebApi } from '@/entrypoints/popup/context/WebApiContext'; import type { EncryptionKeyDerivationParams } from '@/utils/dist/core/models/metadata'; import type { VaultResponse } from '@/utils/dist/core/models/webapi'; import { EncryptionUtility } from '@/utils/EncryptionUtility'; +import { ApiAuthError } from '@/utils/types/errors/ApiAuthError'; import { NetworkError } from '@/utils/types/errors/NetworkError'; import { VaultVersionIncompatibleError } from '@/utils/types/errors/VaultVersionIncompatibleError'; import type { VaultUploadResponse } from '@/utils/types/messaging/VaultUploadResponse'; @@ -303,6 +304,11 @@ export const useVaultSync = (): { syncVault: (options?: VaultSyncOptions) => Pro return false; } + // Check if it's an auth error (session expired) - logout is already triggered by WebApiService + if (err instanceof ApiAuthError) { + return false; + } + // Check if it's a network error - enter offline mode if we have a local vault if (err instanceof NetworkError) { if (dbContext.dbAvailable) { diff --git a/apps/browser-extension/src/entrypoints/popup/pages/auth/Unlock.tsx b/apps/browser-extension/src/entrypoints/popup/pages/auth/Unlock.tsx index 9fe1030d8..6629e1ef3 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/auth/Unlock.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/auth/Unlock.tsx @@ -81,24 +81,34 @@ const Unlock: React.FC = () => { * Returns { online: boolean, error: string | null } */ const checkStatus = async () : Promise<{ online: boolean; error: string | null }> => { - const statusResponse = await webApi.getStatus(); + try { + const statusResponse = await webApi.getStatus(); + + // Server is offline (network error) - this is OK for unlock, we can use local vault + if (statusResponse.serverVersion === '0.0.0') { + setIsInitialLoading(false); + await dbContext.setIsOffline(true); + return { online: false, error: null }; + } + + const statusError = webApi.validateStatusResponse(statusResponse); + if (statusError !== null) { + await app.logout(t('common.errors.' + statusError)); + return { online: false, error: statusError }; + } - // Server is offline - this is OK for unlock, we can use local vault - if (statusResponse.serverVersion === '0.0.0') { setIsInitialLoading(false); - await dbContext.setIsOffline(true); - return { online: false, error: null }; + await dbContext.setIsOffline(false); + return { online: true, error: null }; + } catch { + /** + * Non-network errors (e.g., session expired, auth failures) are thrown by getStatus(). + * The logout event is already emitted by the WebApiService, so we just return an error + * and don't set offline mode since the server is reachable. + */ + setIsInitialLoading(false); + return { online: false, error: 'sessionExpired' }; } - - const statusError = webApi.validateStatusResponse(statusResponse); - if (statusError !== null) { - await app.logout(t('common.errors.' + statusError)); - return { online: false, error: statusError }; - } - - setIsInitialLoading(false); - await dbContext.setIsOffline(false); - return { online: true, error: null }; }; /** diff --git a/apps/browser-extension/src/utils/WebApiService.ts b/apps/browser-extension/src/utils/WebApiService.ts index ce6ed9f78..dca853d1b 100644 --- a/apps/browser-extension/src/utils/WebApiService.ts +++ b/apps/browser-extension/src/utils/WebApiService.ts @@ -3,6 +3,7 @@ import type { StatusResponse } from '@/utils/dist/core/models/webapi'; import { logoutEventEmitter } from '@/events/LogoutEventEmitter'; import { AppInfo } from "./AppInfo"; +import { ApiAuthError } from './types/errors/ApiAuthError'; import { NetworkError } from './types/errors/NetworkError'; import { storage } from '#imports'; @@ -72,13 +73,13 @@ export class WebApiService { }); if (!retryResponse.ok) { - throw new Error('Request failed after token refresh'); + throw new ApiAuthError('Request failed after token refresh'); } return parseJson ? retryResponse.json() : retryResponse as unknown as T; } else { logoutEventEmitter.emit('auth.errors.sessionExpired'); - throw new Error('Session expired'); + throw new ApiAuthError('Session expired'); } } @@ -211,15 +212,21 @@ export class WebApiService { /** * Calls the status endpoint to check if the auth tokens are still valid, app is supported and the vault is up to date. + * Returns offline indicator (serverVersion: '0.0.0') for network failures and server errors (5xx, 404, etc.). + * Auth errors (ApiAuthError) are re-thrown to be handled appropriately (e.g., trigger logout). */ public async getStatus(): Promise { try { return await this.get('Auth/status'); - } catch { + } catch (error) { /** - * If the status endpoint is not available, return a default status response which will trigger - * a logout and error message. + * Only re-throw ApiAuthError (session expired, auth failures). + * All other errors (NetworkError, HTTP 5xx, 404, etc.) indicate the server + * is unreachable or misconfigured, so return offline indicator. */ + if (error instanceof ApiAuthError) { + throw error; + } return { clientVersionSupported: true, serverVersion: '0.0.0',