⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

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

避坑指南

  • 不要无限重试:设置最大重试次数,避免永远卡住。
  • 区分错误类型:服务端错误可以重试,验证错误不应重试。
  • 提供取消选项:让用户能够取消正在进行的上传。