fix(ops-web): stop desktop release upload timing out at complete step
Frontend CI / Frontend (push) Successful in 2m30s

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 09:31:11 +08:00
parent 5af98633ca
commit bb2ad3f6e9
2 changed files with 10 additions and 3 deletions
@@ -77,6 +77,8 @@ export interface DesktopClientReleaseDownloadURLResult {
const DESKTOP_CLIENT_RELEASE_UPLOAD_TIMEOUT_MS = 30 * 60 * 1000 const DESKTOP_CLIENT_RELEASE_UPLOAD_TIMEOUT_MS = 30 * 60 * 1000
const DESKTOP_CLIENT_RELEASE_CHUNK_TIMEOUT_MS = 5 * 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_SIZE_BYTES = 16 * 1024 * 1024
const DESKTOP_CLIENT_RELEASE_CHUNK_RETRIES = 2 const DESKTOP_CLIENT_RELEASE_CHUNK_RETRIES = 2
@@ -257,6 +259,8 @@ async function uploadOSSInChunks(
}) })
return await http.post<DesktopClientReleaseUploadResult>( return await http.post<DesktopClientReleaseUploadResult>(
`/desktop-client/release-uploads/${session.upload_id}/complete`, `/desktop-client/release-uploads/${session.upload_id}/complete`,
undefined,
{ timeout: DESKTOP_CLIENT_RELEASE_COMPLETE_TIMEOUT_MS },
) )
} catch (error) { } catch (error) {
void http.delete(`/desktop-client/release-uploads/${session.upload_id}`).catch(() => undefined) void http.delete(`/desktop-client/release-uploads/${session.upload_id}`).catch(() => undefined)
+6 -3
View File
@@ -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' import { clearStoredSession, readStoredSession } from '@/lib/storage'
@@ -77,6 +77,8 @@ client.interceptors.response.use(
const status = error.response?.status const status = error.response?.status
const payload = error.response?.data const payload = error.response?.data
const handled = status === 401 && !isLoginRequest(error.config?.url, error.config?.method) const handled = status === 401 && !isLoginRequest(error.config?.url, error.config?.method)
const isTimeout =
!error.response && (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT')
if (handled) { if (handled) {
clearStoredSession() clearStoredSession()
@@ -88,7 +90,7 @@ client.interceptors.response.use(
return Promise.reject( return Promise.reject(
new OpsApiError({ 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, code: typeof payload?.code === 'number' ? payload.code : undefined,
status, status,
detail: typeof payload?.detail === 'string' ? payload.detail : undefined, detail: typeof payload?.detail === 'string' ? payload.detail : undefined,
@@ -114,7 +116,8 @@ async function unwrap<T>(promise: Promise<{ data: ApiEnvelope<T> }>): Promise<T>
export const http = { export const http = {
get: <T>(url: string, params?: Record<string, unknown>) => unwrap<T>(client.get(url, { params })), get: <T>(url: string, params?: Record<string, unknown>) => unwrap<T>(client.get(url, { params })),
post: <T, B = unknown>(url: string, body?: B) => unwrap<T>(client.post(url, body)), post: <T, B = unknown>(url: string, body?: B, config?: AxiosRequestConfig) =>
unwrap<T>(client.post(url, body, config)),
put: <T, B = unknown>(url: string, body?: B) => unwrap<T>(client.put(url, body)), put: <T, B = unknown>(url: string, body?: B) => unwrap<T>(client.put(url, body)),
patch: <T, B = unknown>(url: string, body?: B) => unwrap<T>(client.patch(url, body)), patch: <T, B = unknown>(url: string, body?: B) => unwrap<T>(client.patch(url, body)),
delete: <T>(url: string) => unwrap<T>(client.delete(url)), delete: <T>(url: string) => unwrap<T>(client.delete(url)),