diff --git a/lib/client/upload-with-retry.ts b/lib/client/upload-with-retry.ts index 095bf703c..4690aad1c 100644 --- a/lib/client/upload-with-retry.ts +++ b/lib/client/upload-with-retry.ts @@ -10,6 +10,7 @@ const CLIENT_UPLOAD_ID_FIELD = "clientUploadId"; const DIRECT_UPLOAD_INITIATE_TIMEOUT_MS = 3 * 1000; const DIRECT_UPLOAD_ACTION_TIMEOUT_MS = 15 * 1000; const DIRECT_UPLOAD_ABORT_TIMEOUT_MS = 8 * 1000; +const DIRECT_UPLOAD_STALL_TIMEOUT_MS = 10 * 1000; const XHR_UPLOAD_TOTAL_TIMEOUT_MS = 6 * 60 * 1000; const XHR_UPLOAD_STALL_TIMEOUT_MS = 30 * 1000; @@ -336,6 +337,7 @@ async function fetchWithControlTimeout( const controller = new AbortController(); let timeout: ReturnType | null = null; let timedOut = false; + let settled = false; const onAbort = () => { controller.abort(); @@ -344,30 +346,44 @@ async function fetchWithControlTimeout( signal?.addEventListener("abort", onAbort, { once: true }); try { - timeout = setTimeout(() => { - timedOut = true; - controller.abort(); - }, timeoutMs); - - return await fetch(input, { + const request = fetch(input, { ...init, signal: controller.signal, + }).catch((error) => { + if (signal?.aborted) { + throw createAbortError(); + } + + if (timedOut) { + throw createTimeoutError( + `Direct upload control request timed out after ${Math.round( + timeoutMs / 1000 + )}s` + ); + } + + throw error; }); - } catch (error) { - if (signal?.aborted) { - throw createAbortError(); - } - if (timedOut) { - throw createTimeoutError( - `Direct upload control request timed out after ${Math.round( - timeoutMs / 1000 - )}s` - ); - } + const timeoutPromise = new Promise((_, reject) => { + timeout = setTimeout(() => { + timedOut = true; + controller.abort(); + if (!settled) { + reject( + createTimeoutError( + `Direct upload control request timed out after ${Math.round( + timeoutMs / 1000 + )}s` + ) + ); + } + }, timeoutMs); + }); - throw error; + return await Promise.race([request, timeoutPromise]); } finally { + settled = true; if (timeout) { clearTimeout(timeout); } @@ -547,7 +563,8 @@ async function fetchDirectPartWithRetry( }, maxRetries: number, signal?: AbortSignal, - onProgress?: (loaded: number, total: number) => void + onProgress?: (loaded: number, total: number) => void, + options?: { stallTimeoutMs?: number } ) { for (let attempt = 0; ; attempt += 1) { try { @@ -557,6 +574,7 @@ async function fetchDirectPartWithRetry( { onProgress, signal, + stallTimeoutMs: options?.stallTimeoutMs, } ); @@ -667,7 +685,8 @@ async function uploadMultipartParts(params: { params.signal, (loaded, total) => { reportPartProgress(part.partNumber, loaded, total || body.size); - } + }, + { stallTimeoutMs: DIRECT_UPLOAD_STALL_TIMEOUT_MS } ); if (!response.ok) { @@ -731,6 +750,9 @@ async function fetchDirectUpload( ); if (upload.mode === "single") { + emitUploadProgress(onProgress, 0, file.blob.size, "direct", { + percent: 2, + }); const response = await fetchDirectPartWithRetry( upload.url, { @@ -744,7 +766,8 @@ async function fetchDirectUpload( signal, (loaded, total) => { emitUploadProgress(onProgress, loaded, total || file.blob.size, "direct"); - } + }, + { stallTimeoutMs: DIRECT_UPLOAD_STALL_TIMEOUT_MS } ); if (!response.ok) { @@ -774,6 +797,9 @@ async function fetchDirectUpload( } multipartUpload = upload; + emitUploadProgress(onProgress, 0, file.blob.size, "multipart", { + percent: 2, + }); const parts = await uploadMultipartParts({ file, upload,