fix(auth): 为登录链路增加有界超时
ci / verify (pull_request) Successful in 11m22s
ci / verify (pull_request) Successful in 11m22s
为 PostgreSQL 连接、就绪检查和本地登录设置分层超时,数据库不可用时返回稳定 503 错误码并记录无凭据的连接池统计。 前端登录在 10 秒后取消请求并兼容调用方 AbortSignal,Nginx 登录精确路由限制上游为 15 秒,同时更新 OpenAPI 和回归测试。
This commit is contained in:
@@ -9,12 +9,39 @@ import {
|
||||
getAPITask,
|
||||
getCurrentUser,
|
||||
getOpsManagementSkillMetadata,
|
||||
loginLocalAccount,
|
||||
OIDC_BROWSER_SESSION_CREDENTIAL,
|
||||
startIdentityPairing,
|
||||
retireIdentityPairingSecurityEventConflict,
|
||||
validateIdentityRevision,
|
||||
} from './api';
|
||||
|
||||
describe('local login transport', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('aborts after ten seconds and returns a stable login timeout message', async () => {
|
||||
vi.useFakeTimers();
|
||||
const fetchMock = vi.fn((_url: string, init?: RequestInit) => new Promise<Response>((_resolve, reject) => {
|
||||
init?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')));
|
||||
}));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const login = loginLocalAccount({ account: 'timeout-test-account', password: 'timeout-test-password' });
|
||||
const rejection = expect(login).rejects.toMatchObject({
|
||||
message: '登录请求超时,请稍后重试',
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
await rejection;
|
||||
|
||||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(init.signal).toBeInstanceOf(AbortSignal);
|
||||
expect(init.signal?.aborted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gateway provisioning errors', () => {
|
||||
const cases = [
|
||||
['GATEWAY_USER_NOT_PROVISIONED', '该账号尚未开通 EasyAI Gateway'],
|
||||
|
||||
+30
-15
@@ -121,6 +121,7 @@ export async function loginLocalAccount(input: { account: string; password: stri
|
||||
auth: false,
|
||||
body: input,
|
||||
method: 'POST',
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1206,7 +1207,7 @@ export async function deleteFileStorageChannel(token: string, channelId: string)
|
||||
|
||||
async function request<T>(
|
||||
path: string,
|
||||
options: { token?: string; auth?: boolean; method?: string; body?: unknown; headers?: Record<string, string>; signal?: AbortSignal } = {},
|
||||
options: { token?: string; auth?: boolean; method?: string; body?: unknown; headers?: Record<string, string>; signal?: AbortSignal; timeoutMs?: number } = {},
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = { ...(options.headers ?? {}) };
|
||||
if (options.auth !== false && options.token && options.token !== OIDC_BROWSER_SESSION_CREDENTIAL) {
|
||||
@@ -1215,21 +1216,35 @@ async function request<T>(
|
||||
if (options.body !== undefined) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
method: options.method ?? 'GET',
|
||||
headers,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
credentials: 'include',
|
||||
signal: options.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new GatewayApiError(parseErrorDetails(body, response.status, `Request failed: ${response.status}`));
|
||||
const controller = options.timeoutMs ? new AbortController() : undefined;
|
||||
const timeout = controller ? globalThis.setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
|
||||
const signal = controller && options.signal
|
||||
? AbortSignal.any([controller.signal, options.signal])
|
||||
: controller?.signal ?? options.signal;
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
method: options.method ?? 'GET',
|
||||
headers,
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
credentials: 'include',
|
||||
signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
throw new GatewayApiError(parseErrorDetails(body, response.status, `Request failed: ${response.status}`));
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
} catch (error) {
|
||||
if (controller?.signal.aborted) {
|
||||
throw new GatewayApiError('登录请求超时,请稍后重试');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (timeout !== undefined) globalThis.clearTimeout(timeout);
|
||||
}
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
function authorizationHeader(token: string): Record<string, string> {
|
||||
|
||||
Reference in New Issue
Block a user