fix and update
This commit is contained in:
@@ -494,7 +494,7 @@ export async function approveOAuthAuthorization(params: URLSearchParams, token:
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
}, token);
|
||||
}, token, true, { critical: true });
|
||||
}
|
||||
|
||||
export async function checkOAuthConsent(params: URLSearchParams, token: string) {
|
||||
@@ -503,11 +503,17 @@ export async function checkOAuthConsent(params: URLSearchParams, token: string)
|
||||
const scope = params.get('scope') ?? 'openid profile';
|
||||
if (clientId) query.set('client_id', clientId);
|
||||
query.set('scope', scope);
|
||||
return apiFetch<OAuthConsentCheckResponse>(`/oauth/consent/check?${query.toString()}`, {}, token);
|
||||
return apiFetch<OAuthConsentCheckResponse>(`/oauth/consent/check?${query.toString()}`, {}, token, true, { critical: true });
|
||||
}
|
||||
|
||||
export async function fetchOAuthClientPublicInfo(clientId: string) {
|
||||
return apiFetch<{ clientId: string; name: string }>(`/oauth/clients/${encodeURIComponent(clientId)}/public`);
|
||||
return apiFetch<{ clientId: string; name: string }>(
|
||||
`/oauth/clients/${encodeURIComponent(clientId)}/public`,
|
||||
{},
|
||||
null,
|
||||
true,
|
||||
{ silent: true }
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchUserOAuthConsents(userId: string, token: string) {
|
||||
@@ -552,7 +558,9 @@ export async function syncFedcmSession(accessToken?: string | null) {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
},
|
||||
token
|
||||
token,
|
||||
true,
|
||||
{ silent: true }
|
||||
);
|
||||
} catch {
|
||||
// фоновая синхронизация FedCM cookie
|
||||
@@ -681,6 +689,14 @@ export function buildSystemSettingPayload(setting: Pick<SystemSetting, 'key' | '
|
||||
|
||||
const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]);
|
||||
const GATEWAY_RETRY_ATTEMPTS = 2;
|
||||
const GATEWAY_CRITICAL_RETRY_ATTEMPTS = 4;
|
||||
|
||||
export interface ApiFetchBehavior {
|
||||
/** Пользовательский сценарий (OAuth, вход) — не блокировать circuit breaker. */
|
||||
critical?: boolean;
|
||||
/** Опциональный/фоновый запрос — не открывать circuit breaker при ошибке. */
|
||||
silent?: boolean;
|
||||
}
|
||||
const GATEWAY_STABLE_PROBE_INTERVAL_MS = 800;
|
||||
const GATEWAY_STABLE_MAX_ROUNDS = 8;
|
||||
const API_MAX_CONCURRENT = 6;
|
||||
@@ -710,6 +726,11 @@ export function resetApiGatewayWarmup() {
|
||||
gatewayCircuitOpenUntil = 0;
|
||||
}
|
||||
|
||||
/** Сбрасывает circuit breaker после кратковременных 502 — для OAuth и других интерактивных экранов. */
|
||||
export function resetGatewayCircuit(): void {
|
||||
gatewayCircuitOpenUntil = 0;
|
||||
}
|
||||
|
||||
export function invalidateApiGatewayReady() {
|
||||
gatewayReadyResolved = false;
|
||||
gatewayReadyAt = 0;
|
||||
@@ -719,7 +740,12 @@ export function invalidateApiGatewayReady() {
|
||||
}
|
||||
}
|
||||
|
||||
function openGatewayCircuit(): void {
|
||||
function shouldBypassGatewayCircuit(behavior?: ApiFetchBehavior): boolean {
|
||||
return Boolean(behavior?.critical || behavior?.silent);
|
||||
}
|
||||
|
||||
function openGatewayCircuit(behavior?: ApiFetchBehavior): void {
|
||||
if (shouldBypassGatewayCircuit(behavior)) return;
|
||||
gatewayReadyResolved = false;
|
||||
gatewayReadyAt = 0;
|
||||
stableGatewayPromise = null;
|
||||
@@ -729,8 +755,8 @@ function openGatewayCircuit(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function assertGatewayCircuitClosed(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
function assertGatewayCircuitClosed(behavior?: ApiFetchBehavior): void {
|
||||
if (typeof window === 'undefined' || shouldBypassGatewayCircuit(behavior)) return;
|
||||
if (Date.now() < gatewayCircuitOpenUntil) {
|
||||
throw new ApiError('Сервер API временно недоступен. Запрос отложен, чтобы не перегружать консоль ошибками.', 503, 'GATEWAY_UNAVAILABLE');
|
||||
}
|
||||
@@ -863,21 +889,26 @@ function gatewayRetryDelay(attempt: number): number {
|
||||
}
|
||||
|
||||
/** Повтор при кратковременном 502/503/504 без сброса всего UI в bootstrap. */
|
||||
async function fetchWithGatewayRetry(url: string, init: RequestInit): Promise<Response> {
|
||||
async function fetchWithGatewayRetry(
|
||||
url: string,
|
||||
init: RequestInit,
|
||||
behavior?: ApiFetchBehavior
|
||||
): Promise<Response> {
|
||||
const maxAttempts = behavior?.critical ? GATEWAY_CRITICAL_RETRY_ATTEMPTS : GATEWAY_RETRY_ATTEMPTS;
|
||||
let lastNetworkError: unknown;
|
||||
for (let attempt = 0; attempt < GATEWAY_RETRY_ATTEMPTS; attempt++) {
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
try {
|
||||
assertGatewayCircuitClosed();
|
||||
assertGatewayCircuitClosed(behavior);
|
||||
const response = await fetch(url, init);
|
||||
if (response.ok) {
|
||||
markApiGatewayReady();
|
||||
}
|
||||
if (GATEWAY_RETRY_STATUS.has(response.status) && attempt + 1 < GATEWAY_RETRY_ATTEMPTS) {
|
||||
if (GATEWAY_RETRY_STATUS.has(response.status) && attempt + 1 < maxAttempts) {
|
||||
await sleep(gatewayRetryDelay(attempt));
|
||||
continue;
|
||||
}
|
||||
if (GATEWAY_RETRY_STATUS.has(response.status)) {
|
||||
openGatewayCircuit();
|
||||
openGatewayCircuit(behavior);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
@@ -885,19 +916,25 @@ async function fetchWithGatewayRetry(url: string, init: RequestInit): Promise<Re
|
||||
if (isGatewayUnavailableError(error)) {
|
||||
throw error;
|
||||
}
|
||||
if (attempt + 1 < GATEWAY_RETRY_ATTEMPTS) {
|
||||
if (attempt + 1 < maxAttempts) {
|
||||
await sleep(gatewayRetryDelay(attempt));
|
||||
continue;
|
||||
}
|
||||
openGatewayCircuit();
|
||||
openGatewayCircuit(behavior);
|
||||
throw mapFetchError(error);
|
||||
}
|
||||
}
|
||||
openGatewayCircuit();
|
||||
openGatewayCircuit(behavior);
|
||||
throw mapFetchError(lastNetworkError);
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(path: string, options: RequestInit = {}, token?: string | null, allowRetry = true): Promise<T> {
|
||||
export async function apiFetch<T>(
|
||||
path: string,
|
||||
options: RequestInit = {},
|
||||
token?: string | null,
|
||||
allowRetry = true,
|
||||
behavior: ApiFetchBehavior = {}
|
||||
): Promise<T> {
|
||||
await acquireApiSlot();
|
||||
|
||||
const resolvedToken = resolveAccessToken(token);
|
||||
@@ -924,7 +961,7 @@ export async function apiFetch<T>(path: string, options: RequestInit = {}, token
|
||||
response = await fetchWithGatewayRetry(`${getApiUrl()}${path}`, {
|
||||
...options,
|
||||
headers
|
||||
});
|
||||
}, behavior);
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError) {
|
||||
throw error;
|
||||
@@ -949,7 +986,7 @@ export async function apiFetch<T>(path: string, options: RequestInit = {}, token
|
||||
const refreshed = await trySilentTokenRefresh();
|
||||
if (refreshed?.accessToken) {
|
||||
releaseCurrentApiSlot();
|
||||
return apiFetch<T>(path, options, refreshed.accessToken, false);
|
||||
return apiFetch<T>(path, options, refreshed.accessToken, false, behavior);
|
||||
}
|
||||
if (refreshed?.requiresPin) {
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user