From bb2ad3f6e924c4c13fc71d155530239cadf45f96 Mon Sep 17 00:00:00 2001 From: liangxu Date: Thu, 11 Jun 2026 09:31:11 +0800 Subject: [PATCH] fix(ops-web): stop desktop release upload timing out at complete step The chunked upload complete request used the global 30s axios timeout, but the server merges chunks, hashes the file and writes ~100MB to OSS in that step, so large installers failed with "timeout of 30000ms exceeded" after all chunks were uploaded. - give the complete request a dedicated 10min timeout - let http.post pass through an AxiosRequestConfig - normalize client-side timeout errors to "request_timeout" Co-Authored-By: Claude Fable 5 --- apps/ops-web/src/lib/desktop-client-releases.ts | 4 ++++ apps/ops-web/src/lib/http.ts | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/ops-web/src/lib/desktop-client-releases.ts b/apps/ops-web/src/lib/desktop-client-releases.ts index feb982f..cfc6483 100644 --- a/apps/ops-web/src/lib/desktop-client-releases.ts +++ b/apps/ops-web/src/lib/desktop-client-releases.ts @@ -77,6 +77,8 @@ export interface DesktopClientReleaseDownloadURLResult { const DESKTOP_CLIENT_RELEASE_UPLOAD_TIMEOUT_MS = 30 * 60 * 1000 const DESKTOP_CLIENT_RELEASE_CHUNK_TIMEOUT_MS = 5 * 60 * 1000 +// complete 阶段服务端需要合并分片、计算 SHA256 并把整个安装包上传到 OSS,远超默认 30s +const DESKTOP_CLIENT_RELEASE_COMPLETE_TIMEOUT_MS = 10 * 60 * 1000 const DESKTOP_CLIENT_RELEASE_CHUNK_SIZE_BYTES = 16 * 1024 * 1024 const DESKTOP_CLIENT_RELEASE_CHUNK_RETRIES = 2 @@ -257,6 +259,8 @@ async function uploadOSSInChunks( }) return await http.post( `/desktop-client/release-uploads/${session.upload_id}/complete`, + undefined, + { timeout: DESKTOP_CLIENT_RELEASE_COMPLETE_TIMEOUT_MS }, ) } catch (error) { void http.delete(`/desktop-client/release-uploads/${session.upload_id}`).catch(() => undefined) diff --git a/apps/ops-web/src/lib/http.ts b/apps/ops-web/src/lib/http.ts index 844dc29..f517c4a 100644 --- a/apps/ops-web/src/lib/http.ts +++ b/apps/ops-web/src/lib/http.ts @@ -1,4 +1,4 @@ -import axios, { type AxiosError, type AxiosInstance } from 'axios' +import axios, { type AxiosError, type AxiosInstance, type AxiosRequestConfig } from 'axios' import { clearStoredSession, readStoredSession } from '@/lib/storage' @@ -77,6 +77,8 @@ client.interceptors.response.use( const status = error.response?.status const payload = error.response?.data const handled = status === 401 && !isLoginRequest(error.config?.url, error.config?.method) + const isTimeout = + !error.response && (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') if (handled) { clearStoredSession() @@ -88,7 +90,7 @@ client.interceptors.response.use( return Promise.reject( new OpsApiError({ - message: payload?.message ?? error.message ?? 'request_failed', + message: payload?.message ?? (isTimeout ? 'request_timeout' : error.message) ?? 'request_failed', code: typeof payload?.code === 'number' ? payload.code : undefined, status, detail: typeof payload?.detail === 'string' ? payload.detail : undefined, @@ -114,7 +116,8 @@ async function unwrap(promise: Promise<{ data: ApiEnvelope }>): Promise export const http = { get: (url: string, params?: Record) => unwrap(client.get(url, { params })), - post: (url: string, body?: B) => unwrap(client.post(url, body)), + post: (url: string, body?: B, config?: AxiosRequestConfig) => + unwrap(client.post(url, body, config)), put: (url: string, body?: B) => unwrap(client.put(url, body)), patch: (url: string, body?: B) => unwrap(client.patch(url, body)), delete: (url: string) => unwrap(client.delete(url)),