From 4548cd98bbe83cccdae7189db4de392cbc7494cb Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 7 Feb 2026 16:59:29 +0100 Subject: [PATCH] Update browser extension auth to properly differentiate between network errors and auth errors (#1644) --- .../src/utils/WebApiService.ts | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/apps/browser-extension/src/utils/WebApiService.ts b/apps/browser-extension/src/utils/WebApiService.ts index dea36f63d..cb8f447e2 100644 --- a/apps/browser-extension/src/utils/WebApiService.ts +++ b/apps/browser-extension/src/utils/WebApiService.ts @@ -64,9 +64,11 @@ export class WebApiService { const response = await this.rawFetch(endpoint, requestOptions); if (response.status === 401) { - const newToken = await this.refreshAccessToken(); - if (newToken) { - headers.set('Authorization', `Bearer ${newToken}`); + const refreshResult = await this.refreshAccessToken(); + + if (refreshResult.token) { + // Token refresh succeeded - retry the request + headers.set('Authorization', `Bearer ${refreshResult.token}`); const retryResponse = await this.rawFetch(endpoint, { ...requestOptions, headers, @@ -77,9 +79,13 @@ export class WebApiService { } return parseJson ? retryResponse.json() : retryResponse as unknown as T; - } else { + } else if (refreshResult.isAuthError) { + // Token refresh failed due to auth error (401/403) - session is truly expired logoutEventEmitter.emit('common.errors.sessionExpired'); throw new ApiAuthError('Session expired'); + } else { + // Token refresh failed due to network/server error - throw NetworkError for offline handling + throw new NetworkError('Token refresh failed due to network error'); } } @@ -252,12 +258,16 @@ export class WebApiService { } /** - * Refresh the access token. + * Result of a token refresh attempt. + * - token: New access token if refresh succeeded + * - isAuthError: True if refresh failed due to auth error (401/403), meaning session is truly expired + * False if refresh failed due to network/server error, meaning we should enter offline mode */ - private async refreshAccessToken(): Promise { + private async refreshAccessToken(): Promise<{ token: string | null; isAuthError: boolean }> { const refreshToken = await this.getRefreshToken(); if (!refreshToken) { - return null; + // No refresh token means session is truly expired + return { token: null, isAuthError: true }; } try { @@ -273,16 +283,30 @@ export class WebApiService { }), }); - if (!response.ok) { - throw new Error('Failed to refresh token'); + if (response.ok) { + const tokenResponse: TokenResponse = await response.json(); + this.updateTokens(tokenResponse.token, tokenResponse.refreshToken); + return { token: tokenResponse.token, isAuthError: false }; } - const tokenResponse: TokenResponse = await response.json(); - this.updateTokens(tokenResponse.token, tokenResponse.refreshToken); - return tokenResponse.token; - } catch { - logoutEventEmitter.emit('common.errors.sessionExpired'); - return null; + // Auth errors (401/403) mean session is truly expired + if (response.status === 401 || response.status === 403) { + return { token: null, isAuthError: true }; + } + + // Server errors (5xx) or other non-auth errors, treat as offline/transient + console.warn(`Token refresh failed with status ${response.status}, treating as offline`); + return { token: null, isAuthError: false }; + } catch (error) { + // Network errors (server unreachable, timeout, DNS, etc.), treat as offline + if (error instanceof NetworkError) { + console.warn('Token refresh failed due to network error, treating as offline'); + return { token: null, isAuthError: false }; + } + + // Unexpected errors, treat as auth error so logout is triggered + console.error('Unexpected error during token refresh:', error); + return { token: null, isAuthError: true }; } }