http: only a 401 from /auth/refresh signs the user out
A 429, 5xx or network failure during token refresh used to sign the user out. Behind one school Wi-Fi or mobile CGNAT the backend's per-IP refresh limit was hit, and phone users were sent back to SMS login. Now those cases keep the session and surface as a retryable error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -163,7 +163,7 @@ export function createHttpClient(options: HttpClientOptions) {
|
|||||||
|
|
||||||
const inFlight = new Map<string, Promise<unknown>>();
|
const inFlight = new Map<string, Promise<unknown>>();
|
||||||
const signedOutListeners = new Set<() => void>();
|
const signedOutListeners = new Set<() => void>();
|
||||||
let refreshing: Promise<Tokens | null> | null = null;
|
let refreshing: Promise<Tokens | 'invalid' | ApiError> | null = null;
|
||||||
|
|
||||||
function authHeaders(): Record<string, string> {
|
function authHeaders(): Record<string, string> {
|
||||||
const token = storage.get()?.token;
|
const token = storage.get()?.token;
|
||||||
@@ -175,27 +175,39 @@ export function createHttpClient(options: HttpClientOptions) {
|
|||||||
signedOutListeners.forEach((listener) => listener());
|
signedOutListeners.forEach((listener) => listener());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshTokens(): Promise<Tokens | null> {
|
/**
|
||||||
|
* نتیجهی تمدید سه حالت دارد و فقط یکیاش یعنی خروج:
|
||||||
|
* • Tokens — تمدید شد.
|
||||||
|
* • 'invalid' — سرور گفت refresh token معتبر نیست (۴۰۱)؛ نشست واقعاً تمام است.
|
||||||
|
* • ApiError — سرور در دسترس نبود یا سقف را زد (۴۲۹، ۵xx، قطعیِ شبکه)؛ نشست سالم است.
|
||||||
|
* تا ۲۰۲۶-۱۰-۰۳ هر شکستی خروج حساب میشد: پشتِ وایفایِ مدرسه که سقفِ تمدیدِ IP پر میشد،
|
||||||
|
* دانشآموزِ شمارهدار به صفحهی ورود پرت میشد و باید دوباره پیامک میگرفت.
|
||||||
|
*/
|
||||||
|
async function refreshTokens(): Promise<Tokens | 'invalid' | ApiError> {
|
||||||
const current = storage.get();
|
const current = storage.get();
|
||||||
if (!current?.refreshToken) return null;
|
if (!current?.refreshToken) return 'invalid';
|
||||||
if (refreshing) return refreshing;
|
if (refreshing) return refreshing;
|
||||||
|
|
||||||
refreshing = (async () => {
|
refreshing = (async (): Promise<Tokens | 'invalid' | ApiError> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${baseUrl}/auth/refresh`, {
|
let response: Response;
|
||||||
method: 'POST',
|
try {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
response = await fetch(`${baseUrl}/auth/refresh`, {
|
||||||
body: JSON.stringify({ refreshToken: current.refreshToken }),
|
method: 'POST',
|
||||||
signal: timeoutSignal(defaultTimeout),
|
headers: { 'Content-Type': 'application/json' },
|
||||||
credentials: 'omit',
|
body: JSON.stringify({ refreshToken: current.refreshToken }),
|
||||||
});
|
signal: timeoutSignal(defaultTimeout),
|
||||||
if (!response.ok) return null;
|
credentials: 'omit',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return new ApiError(0, 'ارتباط با سرور برقرار نشد');
|
||||||
|
}
|
||||||
|
if (response.status === 401) return 'invalid';
|
||||||
|
if (!response.ok) return errorFrom(response, await parseBody(response));
|
||||||
const data = (await response.json()) as Tokens;
|
const data = (await response.json()) as Tokens;
|
||||||
const next = { token: data.token, refreshToken: data.refreshToken || current.refreshToken };
|
const next = { token: data.token, refreshToken: data.refreshToken || current.refreshToken };
|
||||||
storage.set(next);
|
storage.set(next);
|
||||||
return next;
|
return next;
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
} finally {
|
} finally {
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
refreshing = null;
|
refreshing = null;
|
||||||
@@ -243,8 +255,9 @@ export function createHttpClient(options: HttpClientOptions) {
|
|||||||
let response = await send(method, path, body, opts);
|
let response = await send(method, path, body, opts);
|
||||||
if (response.status === 401 && hadToken) {
|
if (response.status === 401 && hadToken) {
|
||||||
const refreshed = await refreshTokens();
|
const refreshed = await refreshTokens();
|
||||||
if (refreshed) response = await send(method, path, body, opts);
|
if (refreshed === 'invalid') signOut();
|
||||||
else signOut();
|
else if (refreshed instanceof ApiError) throw refreshed; // نشست سالم است؛ تکرارپذیر مثلِ خطای شبکه
|
||||||
|
else response = await send(method, path, body, opts);
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user