fix(frontend): fail fast on deterministic refresh errors

This commit is contained in:
elky
2026-09-04 13:24:17 +08:00
parent 47b21a25d3
commit 1e13fa032c
2 changed files with 26 additions and 3 deletions
@@ -198,6 +198,26 @@ describe('CrossTabRefreshCoordinator', () => {
coordinator.destroy()
})
it('does not wait on other deterministic client-side refresh rejections', async () => {
const refreshError = createHttpError(400, 'invalid refresh request')
const executor = vi.fn(() => Promise.reject(refreshError))
const coordinator = new CrossTabRefreshCoordinator({
storage: localStorage,
channelFactory: createChannel,
waitTimeoutMs: 500,
})
const outcome = await Promise.race([
coordinator.run(executor).catch((error: unknown) => error),
new Promise<symbol>((resolve) => setTimeout(() => resolve(Symbol('timeout')), 0)),
])
expect(outcome).toBe(refreshError)
expect(executor).toHaveBeenCalledTimes(1)
coordinator.destroy()
})
it('keeps the coordination window for a refresh-token rotation conflict', async () => {
const refreshError = createHttpError(409, 'refresh token was rotated concurrently')
const executor = vi.fn(() => Promise.reject(refreshError))
+6 -3
View File
@@ -90,7 +90,10 @@ function isDefinitiveRefreshRejection(error: unknown): boolean {
return false
}
const status = (response as { status?: unknown }).status
return status === 401 || status === 403
// Refresh uses 409 exclusively for a previous-token rotation race. Every
// other client-side rejection is deterministic and cannot be repaired by
// waiting for another tab.
return status >= 400 && status < 500 && status !== 409
}
export class CrossTabRefreshCoordinator {
@@ -197,8 +200,8 @@ export class CrossTabRefreshCoordinator {
}
// The refresh endpoint reserves 409 for a concurrent token rotation.
// A direct 401/403 is authoritative, so waiting for the HTTP timeout
// cannot recover the session and would block initial navigation.
// Other 4xx responses are authoritative, so waiting for the HTTP
// timeout cannot recover the session and would block initial navigation.
if (isDefinitiveRefreshRejection(error)) {
throw error
}