12.5.4 上传失败怎么办——错误处理:网络异常与重试机制
一句话破题
网络不可靠,上传必然会失败——关键是用指数退避重试和用户友好的反馈让失败变得"可接受"。
重试策略
typescript
interface RetryConfig {
maxRetries: number;
baseDelay: number;
maxDelay: number;
}
async function withRetry<T>(
fn: () => Promise<T>,
config: RetryConfig = { maxRetries: 3, baseDelay: 1000, maxDelay: 10000 }
): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === config.maxRetries) {
break;
}
// 指数退避 + 随机抖动
const delay = Math.min(
config.baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
config.maxDelay
);
console.log(`重试 ${attempt + 1}/${config.maxRetries},等待 ${delay}ms`);
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastError;
}
// 使用示例
await withRetry(() => uploadChunk(chunk, fileId), {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 10000,
});错误分类处理
typescript
enum UploadErrorType {
NETWORK = 'network',
TIMEOUT = 'timeout',
SERVER = 'server',
VALIDATION = 'validation',
QUOTA = 'quota',
}
function classifyError(error: unknown): UploadErrorType {
if (error instanceof TypeError && error.message.includes('fetch')) {
return UploadErrorType.NETWORK;
}
if (error instanceof DOMException && error.name === 'AbortError') {
return UploadErrorType.TIMEOUT;
}
if (error instanceof Response) {
if (error.status === 413) return UploadErrorType.QUOTA;
if (error.status >= 400 && error.status < 500) return UploadErrorType.VALIDATION;
if (error.status >= 500) return UploadErrorType.SERVER;
}
return UploadErrorType.SERVER;
}
async function handleUploadError(error: unknown, chunk: Chunk): Promise<'retry' | 'skip' | 'abort'> {
const errorType = classifyError(error);
switch (errorType) {
case UploadErrorType.NETWORK:
case UploadErrorType.TIMEOUT:
case UploadErrorType.SERVER:
return 'retry'; // 可重试的错误
case UploadErrorType.VALIDATION:
console.error(`分片 ${chunk.index} 验证失败,跳过`);
return 'skip';
case UploadErrorType.QUOTA:
console.error('存储配额已满');
return 'abort';
default:
return 'abort';
}
}完整上传器实现
typescript
interface UploadProgress {
uploaded: number;
total: number;
percent: number;
speed: number; // bytes/s
remaining: number; // seconds
}
class RobustUploader {
private abortController: AbortController | null = null;
private startTime = 0;
private uploadedBytes = 0;
async upload(
file: File,
onProgress: (progress: UploadProgress) => void,
onError: (error: Error, canRetry: boolean) => void
) {
this.abortController = new AbortController();
this.startTime = Date.now();
this.uploadedBytes = 0;
const chunks = createChunks(file);
const failedChunks: Chunk[] = [];
for (const chunk of chunks) {
if (this.abortController.signal.aborted) {
throw new Error('上传已取消');
}
try {
await withRetry(() => this.uploadChunk(chunk), { maxRetries: 3 });
this.uploadedBytes += chunk.blob.size;
onProgress(this.calculateProgress(file.size));
} catch (error) {
const action = await handleUploadError(error, chunk);
if (action === 'abort') {
onError(error as Error, false);
throw error;
} else if (action === 'retry') {
failedChunks.push(chunk);
}
}
}
// 重试失败的分片
if (failedChunks.length > 0) {
onError(new Error(`${failedChunks.length} 个分片上传失败,尝试重新上传`), true);
for (const chunk of failedChunks) {
await withRetry(() => this.uploadChunk(chunk), { maxRetries: 5 });
}
}
}
cancel() {
this.abortController?.abort();
}
private calculateProgress(totalSize: number): UploadProgress {
const elapsed = (Date.now() - this.startTime) / 1000;
const speed = this.uploadedBytes / elapsed;
const remaining = (totalSize - this.uploadedBytes) / speed;
return {
uploaded: this.uploadedBytes,
total: totalSize,
percent: (this.uploadedBytes / totalSize) * 100,
speed,
remaining,
};
}
}用户友好的错误提示
tsx
function UploadError({ error, onRetry, onCancel }: {
error: Error;
onRetry: () => void;
onCancel: () => void;
}) {
const errorMessages: Record<UploadErrorType, string> = {
network: '网络连接失败,请检查网络后重试',
timeout: '上传超时,请稍后重试',
server: '服务器繁忙,请稍后重试',
validation: '文件格式不正确',
quota: '存储空间不足',
};
return (
<div className="p-4 bg-red-50 border border-red-200 rounded">
<p className="text-red-700">{errorMessages[classifyError(error)]}</p>
<div className="mt-2 flex gap-2">
<button onClick={onRetry} className="px-3 py-1 bg-red-600 text-white rounded">
重试
</button>
<button onClick={onCancel} className="px-3 py-1 border rounded">
取消
</button>
</div>
</div>
);
}AI 协作指南
- 核心意图:让 AI 帮你实现健壮的上传错误处理。
- 需求定义公式:
"请帮我实现一个带有指数退避重试、错误分类和用户友好提示的文件上传组件。" - 关键术语:
指数退避 (exponential backoff)、重试 (retry)、AbortController
避坑指南
- 不要无限重试:设置最大重试次数,避免永远卡住。
- 区分错误类型:服务端错误可以重试,验证错误不应重试。
- 提供取消选项:让用户能够取消正在进行的上传。
