diff --git a/mobile-uni/src/services/api.ts b/mobile-uni/src/services/api.ts index 97638abd..dcb2030b 100644 --- a/mobile-uni/src/services/api.ts +++ b/mobile-uni/src/services/api.ts @@ -118,8 +118,17 @@ export const apiRequest = async (options: RequestOptions): Promise => { return requestWithXhr(options); } + const timeout = options.timeout || 30000; const response = await new Promise((resolve, reject) => { - uni.request({ + let finished = false; + let timer: ReturnType | undefined; + const finish = (callback: () => void) => { + if (finished) return; + finished = true; + if (timer) clearTimeout(timer); + callback(); + }; + const task = uni.request({ url: normalizeUrl(options.url), method: options.method || 'GET', data: options.data as string | ArrayBuffer | Record | undefined, @@ -127,10 +136,19 @@ export const apiRequest = async (options: RequestOptions): Promise => { ...authHeaders(options.method !== 'GET', options.auth !== false), ...options.header }, - timeout: options.timeout || 30000, - success: resolve, - fail: reject + timeout, + success: (result) => finish(() => resolve(result)), + fail: (error) => finish(() => reject(error)) }); + if (!finished) { + timer = setTimeout(() => finish(() => { + try { + task.abort(); + } finally { + reject(new Error('网络请求超时')); + } + }), timeout); + } }); return readPayload(response.statusCode, response.data, options.clearAuthOnUnauthorized !== false); diff --git a/mobile-uni/tests/api-timeout.test.mjs b/mobile-uni/tests/api-timeout.test.mjs new file mode 100644 index 00000000..9c8f6954 --- /dev/null +++ b/mobile-uni/tests/api-timeout.test.mjs @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import { after, test } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { createServer } from 'vite'; + +let abortCount = 0; +globalThis.plus = {}; +globalThis.uni = { + getStorageSync: () => '', + removeStorageSync: () => {}, + request: () => ({ + abort: () => { + abortCount += 1; + } + }) +}; + +const vite = await createServer({ + root: fileURLToPath(new URL('..', import.meta.url)), + configFile: false, + optimizeDeps: { noDiscovery: true }, + server: { middlewareMode: true, hmr: false }, + appType: 'custom' +}); + +after(async () => { + await vite.close(); + delete globalThis.plus; + delete globalThis.uni; +}); + +const { apiRequest } = await vite.ssrLoadModule('/src/services/api.ts'); + +test('App 原生请求遗漏回调时主动超时并中止任务', async () => { + await assert.rejects( + apiRequest({ url: '/api/test', timeout: 10 }), + /网络请求超时/ + ); + assert.equal(abortCount, 1); +});