Skip to content

Commit d00434f

Browse files
authored
Merge pull request #645 from taglia/fix/auth-refresh-retryable-errors
Preserve credentials after retryable token refresh failures
2 parents 12ce0f6 + 2ed9ad4 commit d00434f

2 files changed

Lines changed: 61 additions & 3 deletions

File tree

src/services/auth.service.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ export class AuthService {
107107
);
108108
credentialsChanged = true;
109109
} catch (error) {
110-
await ConfigService.instance.clearUser();
110+
if (this.shouldClearUserAfterRefreshError(error)) {
111+
await ConfigService.instance.clearUser();
112+
}
111113
throw error;
112114
}
113115
}
@@ -126,6 +128,38 @@ export class AuthService {
126128
return loginCreds;
127129
};
128130

131+
private readonly shouldClearUserAfterRefreshError = (error: unknown): boolean => {
132+
if (error instanceof InvalidCredentialsError || error instanceof ExpiredCredentialsError) {
133+
return true;
134+
}
135+
136+
const status = this.getHttpStatusFromError(error);
137+
if (status === undefined) {
138+
return false;
139+
}
140+
141+
return status >= 400 && status < 500 && status !== 408 && status !== 429;
142+
};
143+
144+
private readonly getHttpStatusFromError = (error: unknown): number | undefined => {
145+
if (typeof error !== 'object' || error === null) {
146+
return undefined;
147+
}
148+
149+
const response = 'response' in error ? (error as { response?: { status?: unknown } }).response : undefined;
150+
if (typeof response?.status === 'number') {
151+
return response.status;
152+
}
153+
154+
const status = 'status' in error ? (error as { status?: unknown }).status : undefined;
155+
if (typeof status === 'number') {
156+
return status;
157+
}
158+
159+
const statusCode = 'statusCode' in error ? (error as { statusCode?: unknown }).statusCode : undefined;
160+
return typeof statusCode === 'number' ? statusCode : undefined;
161+
};
162+
129163
/**
130164
* Refreshes the user tokens
131165
*

test/services/auth.service.test.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,14 @@ describe('Auth service', () => {
217217
expect(refreshTokensStub).toHaveBeenCalledOnce();
218218
});
219219

220-
test('when the token refresh fails, then stored credentials are cleared and an error is thrown', async () => {
220+
test('when the token refresh fails with an auth error, then stored credentials are cleared and an error is thrown', async () => {
221221
const sut = AuthService.instance;
222222

223223
const mockTokenStatus = TokenStatus.REFRESH_REQUIRED;
224224

225-
const oldTokenError = new Error('Old token version detected');
225+
const oldTokenError = Object.assign(new Error('Old token version detected'), {
226+
response: { status: 401 },
227+
});
226228

227229
vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture);
228230
vi.spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration').mockImplementationOnce(
@@ -236,4 +238,26 @@ describe('Auth service', () => {
236238
expect(refreshTokenStub).toHaveBeenCalledOnce();
237239
expect(clearUserStub).toHaveBeenCalledOnce();
238240
});
241+
242+
test('when the token refresh fails with a retryable server error, then stored credentials are preserved', async () => {
243+
const sut = AuthService.instance;
244+
245+
const mockTokenStatus = TokenStatus.REFRESH_REQUIRED;
246+
247+
const retryableError = Object.assign(new Error('Service unavailable'), {
248+
response: { status: 503 },
249+
});
250+
251+
vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture);
252+
vi.spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration').mockImplementationOnce(
253+
() => mockTokenStatus,
254+
);
255+
vi.spyOn(ValidationService.instance, 'validateMnemonic').mockReturnValue(true);
256+
const refreshTokenStub = vi.spyOn(sut, 'refreshUserToken').mockRejectedValue(retryableError);
257+
const clearUserStub = vi.spyOn(ConfigService.instance, 'clearUser').mockResolvedValue();
258+
259+
await expect(() => sut.getAuthDetails()).rejects.toThrow(retryableError);
260+
expect(refreshTokenStub).toHaveBeenCalledOnce();
261+
expect(clearUserStub).not.toHaveBeenCalled();
262+
});
239263
});

0 commit comments

Comments
 (0)