鸿蒙网络请求的错误处理与超时管理
网络请求是几乎每个应用都离不开的基础能力。但在 HarmonyOS 开发中,很多开发者(包括我自己)在早期容易写出"快乐路径"代码——只处理成功的情况,完全忽略失败场景。结果就是:无网络时界面一直转圈、请求超时后应用卡死、并发请求耗尽资源……这些问题在生产环境中会被无限放大。本文从真实踩坑出发,逐步构建一套完整的 HTTP 客户端封装方案,覆盖错误分类、超时控制、自动重试、用户友好提示等所有关键环
踩坑记录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)
预期行为:超时后给出友好提示
实际行为:整个页面卡死,无法进行任何操作
根因:没有设置 connectTimeout 和 readTimeout,请求默认等待直到操作系统级超时。
崩溃模式 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 问题根因汇总
三、完整 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 官方文档
扩展阅读
- RESTful API 最佳实践 —— 接口设计规范
- HTTP/1.1 RFC 7231 —— 协议细节
👇 如果这篇对你有帮助,欢迎点赞、收藏、评论!
这套 HTTP 封装方案已在生产环境稳定运行超过半年,希望也能为你的项目提供保障 🔧
更多推荐




所有评论(0)