| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 后端业务异常统一以 HTTP 200 + JSON 返回;下载接口的文件过期、任务状态异常等都属于此类。 |
| | | * 这里把真实原因提取出来,避免被误报成「分段下载失败(HTTP 200)」。 |
| | | */ |
| | | async function readBusinessError(response: Response): Promise<null | string> { |
| | | const contentType = response.headers.get('content-type') ?? ''; |
| | | if (!contentType.includes('json')) return null; |
| | | try { |
| | | const body = await response.clone().json(); |
| | | if (body && typeof body.msg === 'string' && body.msg) return body.msg; |
| | | } catch { |
| | | // 非标准 JSON 交给调用方按通用错误处理 |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** 业务错误重试无意义(如文件已过期),直接终止。 */ |
| | | function nonRetryable(message: string): Error { |
| | | const error = new Error(message) as Error & { retryable?: boolean }; |
| | | error.retryable = false; |
| | | return error; |
| | | } |
| | | |
| | | async function requestChunk( |
| | | url: string, |
| | | token: string | undefined, |
| | |
| | | signal, |
| | | }); |
| | | if (response.status !== 206) { |
| | | const businessError = await readBusinessError(response); |
| | | if (businessError) throw nonRetryable(businessError); |
| | | const contentType = response.headers.get('content-type') ?? ''; |
| | | if (response.status === 200 |
| | | && (contentType.includes('zip') || contentType.includes('octet-stream'))) { |
| | | // 服务端忽略了 Range,直接回整包;继续分段只会反复下载同一份文件 |
| | | throw nonRetryable('服务器未启用分段下载,请升级后端后重试'); |
| | | } |
| | | throw new Error(`分段下载失败(HTTP ${response.status})`); |
| | | } |
| | | if (response.headers.get('content-range') !== `bytes ${start}-${end}/${total}`) { |
| | |
| | | } |
| | | return bytes; |
| | | } catch (error) { |
| | | if (signal.aborted || attempt === 2) throw error; |
| | | const retryable = (error as { retryable?: boolean }).retryable !== false; |
| | | if (signal.aborted || !retryable || attempt === 2) throw error; |
| | | await new Promise((resolve) => setTimeout(resolve, 750 * (attempt + 1))); |
| | | } |
| | | } |
| | |
| | | const match = /^bytes 0-0\/(\d+)$/.exec(response.headers.get('content-range') ?? ''); |
| | | const total = Number(match?.[1]); |
| | | if (response.status !== 206 || !Number.isSafeInteger(total) || total <= 0) { |
| | | const businessError = await readBusinessError(response); |
| | | if (businessError) throw nonRetryable(businessError); |
| | | throw new Error('无法获取导出文件大小,请刷新任务后重试'); |
| | | } |
| | | await response.arrayBuffer(); |