Handle replay hydration file fetch timeouts

This commit is contained in:
ciat-777
2026-03-21 01:03:03 +08:00
parent 95bf8df499
commit 7c227d8365
2 changed files with 42 additions and 20 deletions
+23
View File
@@ -98,6 +98,29 @@ describe('api proxy test timeout handling', () => {
await expect(promise).resolves.toMatchObject({ message: '请求超时(30s)' });
});
it('times out replay hydration file-content fetches after 30 seconds', async () => {
installPendingFetch();
const getProxyFileContentDataUrl = (api as Record<string, any>).getProxyFileContentDataUrl;
let settled = false;
const handled = getProxyFileContentDataUrl?.('file-metapi-123')
.then(() => ({ ok: true as const }))
.catch((error: Error) => ({ ok: false as const, error }))
.finally(() => {
settled = true;
});
await vi.advanceTimersByTimeAsync(30_000);
expect(settled).toBe(true);
const result = await handled;
expect(result.ok).toBe(false);
if (result.ok) {
throw new Error('Expected replay hydration file-content fetch to time out');
}
expect(result.error.message).toBe('请求超时(30s)');
});
it('loads proxy file content as a data URL for replay hydration', async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(
new Blob([Buffer.from('PDF')], { type: 'application/pdf' }),
+19 -20
View File
@@ -71,7 +71,7 @@ function arrayBufferToBase64(buffer: ArrayBuffer): string {
return btoa(binary);
}
async function request(url: string, options: RequestOptions = {}) {
async function fetchAuthenticatedResponse(url: string, options: RequestOptions = {}): Promise<Response> {
const { timeoutMs = 30_000, signal: externalSignal, ...fetchOptions } = options;
const controller = new AbortController();
let timeoutHandle: ReturnType<typeof setTimeout> | null = setTimeout(() => {
@@ -107,13 +107,12 @@ async function request(url: string, options: RequestOptions = {}) {
if (res.status === 401 || res.status === 403) {
const hadToken = !!getAuthToken(localStorage);
clearAuthSession(localStorage);
if (hadToken) window.location.reload();
if (hadToken && typeof window !== 'undefined' && typeof window.location?.reload === 'function') {
window.location.reload();
}
throw new Error('Session expired');
}
if (!res.ok) {
throw new Error(await extractResponseErrorMessage(res));
}
return res.json();
return res;
} catch (error: any) {
if (error?.name === 'AbortError') {
if (externalSignal?.aborted) throw error;
@@ -129,6 +128,14 @@ async function request(url: string, options: RequestOptions = {}) {
}
}
async function request(url: string, options: RequestOptions = {}) {
const res = await fetchAuthenticatedResponse(url, options);
if (!res.ok) {
throw new Error(await extractResponseErrorMessage(res));
}
return res.json();
}
function buildQueryString(params?: Record<string, string | number | boolean | null | undefined>) {
if (!params) return '';
const searchParams = new URLSearchParams();
@@ -664,22 +671,14 @@ export const api = {
}),
getProxyTestJob: (jobId: string) => request(`/api/test/proxy/jobs/${encodeURIComponent(jobId)}`),
deleteProxyTestJob: (jobId: string) => request(`/api/test/proxy/jobs/${encodeURIComponent(jobId)}`, { method: 'DELETE' }),
getProxyFileContentDataUrl: async (fileId: string) => {
const token = requireAuthToken();
const response = await fetch(`/v1/files/${encodeURIComponent(fileId)}/content`, {
getProxyFileContentDataUrl: async (
fileId: string,
options: Pick<RequestOptions, 'signal' | 'timeoutMs'> = {},
) => {
const response = await fetchAuthenticatedResponse(`/v1/files/${encodeURIComponent(fileId)}/content`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
...options,
});
if (response.status === 401 || response.status === 403) {
const hadToken = !!getAuthToken(localStorage);
clearAuthSession(localStorage);
if (hadToken && typeof window !== 'undefined' && typeof window.location?.reload === 'function') {
window.location.reload();
}
throw new Error('Session expired');
}
if (!response.ok) {
throw new Error(await extractResponseErrorMessage(response));
}