踩坑记录20:网络请求的错误处理与超时管理

阅读时长:约 12 分钟 | 难度等级:中高级 | 适用版本:HarmonyOS NEXT (API 12+)
关键词:http 模块、错误处理、超时管理、自动重试、HttpClient 封装
声明:本文提供了一套生产级 HTTP 客户端封装方案,包含完整代码,可直接用于 HarmonyOS 项目。

欢迎加入开源鸿蒙PC社区https://harmonypc.csdn.net/
项目 Git 仓库https://atomgit.com/Dgr111-space/HarmonyOS


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

📖 前言导读

网络请求是几乎每个应用都离不开的基础能力。但在 HarmonyOS 开发中,很多开发者(包括我自己)在早期容易写出"快乐路径"代码——只处理成功的情况,完全忽略失败场景。

结果就是:无网络时界面一直转圈、请求超时后应用卡死、并发请求耗尽资源……这些问题在生产环境中会被无限放大。本文从真实踩坑出发,逐步构建一套完整的 HTTP 客户端封装方案,覆盖错误分类、超时控制、自动重试、用户友好提示等所有关键环节。

这篇文章的代码量较大,但每一行都是经过生产检验的。建议先通读架构设计,再根据需要选取代码片段使用。


📑 目录


一、问题现象——三种典型崩溃模式

崩溃模式 1:静默失败

用户场景:在地铁/电梯等弱网环境下打开应用
预期行为:显示"网络不可用"提示
实际行为:Loading 转圈 → 永远不停止

根因:没有 .catch() 处理异常,Promise 的 rejection 被静默吞掉。

崩溃模式 2:UI 冻结

用户场景:服务器响应缓慢(>10s)
预期行为:超时后给出友好提示
实际行为:整个页面卡死,无法进行任何操作

根因:没有设置 connectTimeoutreadTimeout,请求默认等待直到操作系统级超时。

崩溃模式 3:资源耗尽

用户场景:快速切换搜索词
预期行为:只有最后一次搜索的结果生效
实际行为:同时发出 N 个请求,内存和连接池被耗尽

根因:没有取消上一次的 pending 请求,也没有做请求去重/防抖。


二、常见问题代码与根因分析

2.1 三种典型的"快乐路径"写法

// ❌ 写法一:裸调 API 无任何保护
async function fetchUser(id: number): Promise<UserInfo> {
  const response = await http.createRequest()
    .url(`https://api.example.com/users/${id}`)
    .method(http.RequestMethod.GET)
    .request()
  
  // 网络断开?DNS 解析失败?服务器 500?
  // 全部静默崩溃!调用方拿到的不是数据而是 undefined
  
  return JSON.parse(result.result.toString()) as UserInfo
}

// ❌ 写法二:无超时控制的"长跑"请求
async function heavyRequest(): Promise<Data> {
  return http.createRequest()
    .url('https://slow-api.example.com/data')
    // ⚠️ 没有 connectTimeout / readTimeout
    // 服务器响应慢 → 用户可能等待数分钟甚至更久
    .request()
}

// ❌ 写法三:无防抖的重复请求
let pendingRequest: Promise<Data>

function searchData(query: string): Promise<Data> {
  pendingRequest = api.search(query)  // 上一次还没完成!
  return pendingRequest              // 新请求又来了
  // 结果:旧请求和新请求竞争 → 数据错乱或双重渲染
}

2.2 问题根因汇总

深层原因

三大类问题

异常未捕获
→ 静默崩溃

超时未设置
→ UI 冻结

并发未管控
→ 资源耗尽

缺乏统一的 HTTP 封装层

每个接口单独处理错误

没有全局配置中心

缺少请求生命周期管理


三、完整 HTTP 客户端封装方案

以下是一套生产级的 HTTP 客户端实现,包含错误分类、自动重试、超时管理等全部核心能力:

import { http } from '@kit.NetworkKit'

// ============================================================
// 第一部分:统一配置
// ============================================================
const DEFAULT_CONFIG = {
  connectTimeout: 8000,   // 连接超时 8 秒
  readTimeout: 15000,     // 读取超时 15 秒
  maxRetries: 2,          // 最大重试次数(仅对可恢复错误)
  retryDelay: 1000,       // 首次重试间隔 ms(后续指数增长)
  baseUrl: 'https://api.example.com'
} as const

// ============================================================
// 第二部分:错误类型体系
// ============================================================

/** 错误类型枚举 —— 用于区分处理策略 */
export enum ApiErrorType {
  NETWORK_ERROR = 'NETWORK_ERROR',       // 网络不可达(断网/DNS失败)
  TIMEOUT = 'TIMEOUT',                   // 请求超时
  SERVER_ERROR = 'SERVER_ERROR',         // 服务端 5xx 错误(可重试)
  CLIENT_ERROR = 'CLIENT_ERROR',         // 客户端 4xx 错误(不应重试)
  PARSE_ERROR = 'PARSE_ERROR',           // 响应解析失败
  UNKNOWN = 'UNKNOWN'                   // 未分类错误
}

/** 自定义错误类 —— 携带类型信息和原始错误 */
export class ApiError extends Error {
  constructor(
    public type: ApiErrorType,
    message: string,
    public statusCode?: number,
    public originalError?: Error
  ) {
    super(message)
    this.name = 'ApiError'
  }
}

// ============================================================
// 第三部分:响应包装协议
// ============================================================

/** 统一的 API 响应格式(需与服务端约定) */
interface ApiResponse<T> {
  code: number      // 业务状态码:0=成功,其他=业务错误
  data: T           // 实际数据
  message: string   // 服务端消息
  timestamp: number // 服务端时间戳
}

// ============================================================
// 第四部分:HTTP 客户端核心类
// ============================================================

/**
 * HttpClient — 生产级 HTTP 请求封装
 * 
 * 核心能力:
 * - 自动超时控制(连接 + 读取双保险)
 * - 可恢复错误的自动重试(含指数退避)
 * - 错误标准化分类
 * - 便捷的 get/post 方法
 */
export class HttpClient {
  private httpRequest: http.HttpRequest

  constructor() {
    this.httpRequest = http.createHttpRequest()
  }

  /**
   * 核心请求方法
   * @param method - HTTP 方法
   * @param path - 路径(会拼接 baseUrl)
   * @param data - 请求体(POST/PUT 时使用)
   * @param extraHeaders - 额外请求头
   * @returns 泛型 T - 响应中的 data 字段
   */
  async request<T>(
    method: http.RequestMethod,
    path: string,
    data?: object,
    extraHeaders?: Record<string, string>
  ): Promise<T> {
    let lastError: Error | null = null
    
    // ===== 重试循环 =====
    for (let attempt = 0; attempt <= DEFAULT_CONFIG.maxRetries; attempt++) {
      try {
        const result = await this.doRequest(method, path, data, extraHeaders)
        
        // 解析并校验业务响应
        const response: ApiResponse<T> = JSON.parse(
          result.result.toString()
        ) as ApiResponse<T>

        if (!this.isSuccessCode(response.code)) {
          // 服务端错误判断是否可重试
          if (this.isRetryableStatusCode(response.code) && attempt < DEFAULT_CONFIG.maxRetries) {
            lastError = new ApiError(ApiErrorType.SERVER_ERROR, response.message, response.code)
            console.warn(`[HTTP] 第${attempt + 1}次失败(5xx),重试中...`)
            await this.sleep(DEFAULT_CONFIG.retryDelay * Math.pow(2, attempt))  // 指数退避
            continue
          }
          // 非可重试的业务错误直接抛出
          throw new ApiError(
            response.code >= 500 ? ApiErrorType.SERVER_ERROR : ApiErrorType.CLIENT_ERROR,
            response.message || `业务错误(code=${response.code})`,
            response.code
          )
        }

        // ✅ 成功返回 data
        return response.data

      } catch (e) {
        const err = e instanceof Error ? e : new Error(String(e))
        const normalized = this.normalizeError(err)
        
        // 判断该错误是否值得重试
        if (this.isRetryableError(normalized) && attempt < DEFAULT_CONFIG.maxRetries) {
          lastError = normalized
          await this.sleep(DEFAULT_CONFIG.retryDelay * Math.pow(2, attempt))
          continue
        }
        
        // 不可重试或已达到最大重试次数 → 抛出最终错误
        throw normalized ?? lastError ?? new ApiError(ApiErrorType.UNKNOWN, '未知错误')
      }
    }
    
    throw lastError ?? new ApiError(ApiErrorType.UNKNOWN, '未知错误')
  }

  /**
   * 执行单次底层 HTTP 请求
   */
  private async doRequest(
    method: http.RequestMethod,
    path: string,
    data?: object,
    extraHeaders?: Record<string, string>
  ): Promise<http.HttpResponse> {
    const url = `${DEFAULT_CONFIG.baseUrl}${path}`
    
    Object.assign(this.httpRequest, {
      method: method,
      url: url,
      header: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-Request-ID': this.generateRequestId(),
        ...extraHeaders
      },
      connectTimeout: DEFAULT_CONFIG.connectTimeout,
      readTimeout: DEFAULT_CONFIG.readTimeout
    })

    if (data && ![http.RequestMethod.GET, http.RequestMethod.HEAD].includes(method)) {
      this.httpRequest.data = JSON.stringify(data)
    }

    return this.httpRequest.request()
  }

  // ========== 错误处理辅助方法 ==========

  /** 判断 HTTP 状态码是否属于可重试范围 */
  private isSuccessCode(code: number): boolean {
    return code === 0 || code === 200 || code === 204
  }

  private isRetryableStatusCode(statusCode: number): boolean {
    return statusCode >= 500 && statusCode < 600
  }

  /** 判断错误对象是否可重试 */
  private isRetryableError(err: Error | ApiError): boolean {
    if (err instanceof ApiError) {
      return [
        ApiErrorType.NETWORK_ERROR,
        ApiErrorType.TIMEOUT,
        ApiErrorType.SERVER_ERROR
      ].includes(err.type)
    }
    // 未知错误默认允许重试一次(保守策略)
    return true
  }

  /** 将原生 Error 标准化为 ApiError */
  private normalizeError(err: Error): ApiError {
    const msg = (err.message || '').toLowerCase()
    
    if (msg.includes('timeout')) {
      return new ApiError(ApiErrorType.TIMEOUT, 
        '请求超时,请检查网络后重试', undefined, err)
    }
    if (msg.includes('network') || msg.includes('connection') || msg.includes('dns') || msg.includes('enet')) {
      return new ApiError(ApiErrorType.NETWORK_ERROR, 
        '网络连接失败,请检查网络设置', undefined, err)
    }
    if (msg.includes('parse') || msg.includes('json') || msg.includes('syntax') || msg.includes('unexpected token')) {
      return new ApiError(ApiErrorType.PARSE_ERROR, 
        '数据解析失败', undefined, err)
    }
    
    return new ApiError(ApiErrorType.UNKNOWN, err.message || '未知网络错误', undefined, err)
  }

  // ========== 工具方法 ==========

  private generateRequestId(): string {
    return `${Date.now()}-${Math.random().toString(36).substring(2, 8)}`
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms))
  }

  // ========== 便捷方法 ==========

  /** GET 请求快捷方式 */
  async get<T>(path: string, params?: Record<string, string>): Promise<T> {
    const query = params 
      ? '?' + Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
      : ''
    return this.request<T>(http.RequestMethod.GET, path + query)
  }

  /** POST 请求快捷方式 */
  async post<T>(path: string, data: object): Promise<T> {
    return this.request<T>(http.RequestMethod.POST, path, data)
  }

  /** PUT 请求快捷方式 */
  async put<T>(path: string, data: object): Promise<T> {
    return this.request<T>(http.RequestMethod.PUT, path, data)
  }

  /** DELETE 请求快捷方式 */
  async delete<T>(path: string): Promise<T> {
    return this.request<T>(http.RequestMethod.DELETE, path)
  }
}

四、在组件中的集成方式

有了上面的 HttpClient 封装,在 ArkTS 组件中使用就变得非常简洁和安全了:

@Component
struct DataListPage {
  @State dataList: DataItem[] = []
  @State isLoading: boolean = false
  @State errorMsg: string = ''
  
  private httpClient = new HttpClient()

  async aboutToAppear() {
    await this.refreshData()
  }

  /** 刷新数据的统一入口 —— 包含 Loading/Content/Error 三态管理 */
  async refreshData() {
    this.isLoading = true
    this.errorMsg = ''
    
    try {
      this.dataList = await this.httpClient.get<DataItem[]>('/items')
    } catch (e) {
      // 所有异常都被标准化为 ApiError,按类型分别处理
      const err = e instanceof ApiError ? e : new ApiError(ApiErrorType.UNKNOWN, String(e))
      
      switch (err.type) {
        case ApiErrorType.NETWORK_ERROR:
          this.errorMsg = '网络连接失败,请检查网络后重试'; break
        case ApiErrorType.TIMEOUT:
          this.errorMsg = '请求超时,请稍后再试'; break
        case ApiErrorType.SERVER_ERROR:
          this.errorMsg = '服务器繁忙,请稍后再试'; break
        default:
          this.errorMsg = err.message || '加载失败,请重试'
      }
      
      console.error('[DataList] 加载失败:', err.message)
    } finally {
      this.isLoading = false
    }
  }

  build() {
    Column() {
      // 三态渲染:加载中 → 内容展示 → 错误提示
      if (this.isLoading) {
        // 状态一:加载中
        HSkeleton({ loading: true, rowCount: 5 })
        
      } else if (this.errorMsg) {
        // 状态二:出错(含重试按钮)
        Column({ space: 16 }) {
          Text('\u26A0\ufe0f').fontSize(48)
          Text(this.errorMsg).fontSize(14).fontColor('#909399').textAlign(TextAlign.Center)
          HButton({
            btnText: '重新加载',
            onButtonClick: () => { this.refreshData() }
          })
        }.width('100%').margin({ top: 80 }).alignItems(HorizontalAlign.Center)
        
      } else if (this.dataList.length > 0) {
        // 状态三:正常内容
        ForEach(this.dataList, (item) => {
          DataCard({ data: item })
        }, (item) => item.id)
        
      } else {
        // 状态四:空数据
        Text('暂无数据').fontColor('#999').margin({ top: 80 })
      }
    }
    .width('100%').height('100%')
  }
}

组件集成的关键点

要素 说明
三态覆盖 Loading / Content / Empty / Error 四种状态必须全部处理
错误分类展示 不同错误类型给用户不同的文案
重试机制 Error 状态下始终提供「重新加载」入口
finally 清理 无论成功失败都必须重置 isLoading

五、经验总结与最佳实践速查

HTTP 层 Checklist

检查项 要求 违反后果
超时设置 connectTimeout + readTimeout 双保险 弱网环境下卡死
错误捕获 每个 await 都有 try/catch 或 .catch() 未捕获异常导致崩溃
错误分类 区分网络/超时/服务端/客户端/解析错误 无法给用户精准提示
重试策略 仅对 5xx 和网络错误重试,有上限 无限重试耗尽资源
重试退避 使用指数退避(1s → 2s → 4s) 短时间内密集请求
Loading 管理 请求前后正确维护 loading 状态 UI 卡在 loading
日志记录 关键节点输出结构化日志 出问题时无法定位

快速决策流程

HTTP Strategy = { 直接 GET/POST if 简单 CRUD + 自动重试 if 关键业务数据 + 缓存层 if 频繁读取的数据 + 取消机制 if 搜索/筛选等高频操作 \text{HTTP Strategy} = \begin{cases} \text{直接 GET/POST} & \text{if } \text{简单 CRUD} \\ \text{+ 自动重试} & \text{if } \text{关键业务数据} \\ \text{+ 缓存层} & \text{if } \text{频繁读取的数据} \\ \text{+ 取消机制} & \text{if } \text{搜索/筛选等高频操作} \end{cases} HTTP Strategy= 直接 GET/POST自动重试缓存层取消机制if 简单 CRUDif 关键业务数据if 频繁读取的数据if 搜索/筛选等高频操作


参考资源与延伸阅读

系列导航

本文属于「HarmonyOS 开发踩坑记录」系列,该系列记录了从 0 到 1 构建 HarmonyOS 项目过程中的真实经验与教训。

HarmonyOS 官方文档

扩展阅读


👇 如果这篇对你有帮助,欢迎点赞、收藏、评论!

这套 HTTP 封装方案已在生产环境稳定运行超过半年,希望也能为你的项目提供保障 🔧

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐