HarmonyOS HTTP 请求治理实战:超时、重试、解析与关闭会话

HTTP 请求问题往往不是“接口调不通”这么简单。上线后常见的是页面一直 loading、错误码没有统一文案、弱网下重复请求、请求结束后没有关闭会话导致资源占用。一个稳定的 HarmonyOS 网络层,应该把请求构造、超时、响应解析、重试和资源释放拆开管理。

请添加图片描述

本文解决:如何统一构造请求,如何给超时和错误码做可解释映射,什么请求可以重试,为什么请求结束后要关闭会话。

1. 网络层先定义责任边界

页面不应该直接拼 URL、Header 和错误文案。页面只提交业务意图,网络层负责把它变成可追踪的请求。

请添加图片描述

模块 负责内容 不负责
RequestBuilder URL、Header、traceId 页面展示
HttpRunner 执行请求和超时 业务解析
ResponseParser 解析状态和数据 发起请求
NetworkAudit 记录请求证据 保存敏感数据

2. Network Kit 资料边界和工程目录

HarmonyOS 网络请求通常通过 Network Kit 相关能力完成。工程上重点不是把 API 直接塞进页面,而是建立自己的请求契约。

资料入口 工程落点
Network Kit 网络能力总体边界
HTTP 数据请求 创建请求、发送、响应和销毁
网络管理 网络状态和异常处理
entry/src/main/ets/common/network/
  RequestBuilder.ets
  HttpRunner.ets
  ResponseParser.ets
  RetryPolicy.ets
  NetworkAudit.ets

3. RequestBuilder 统一请求模型

export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'

export interface ApiRequest {
  method: HttpMethod
  path: string
  query?: Record<string, string>
  body?: Record<string, Object>
  traceId: string
}

export class RequestBuilder {
  build(method: HttpMethod, path: string, body?: Record<string, Object>): ApiRequest {
    return {
      method,
      path,
      body,
      traceId: `net_${Date.now()}_${Math.floor(Math.random() * 10000)}`
    }
  }
}

traceId 是排查关键。用户反馈某次请求失败时,服务端和客户端可以用同一个标识串起来。

4. HttpRunner 控制超时和执行结果

export type HttpResult =
  | { ok: true; status: number; data: string; traceId: string }
  | { ok: false; code: 'TIMEOUT' | 'NETWORK' | 'SERVER'; message: string; traceId: string }

export class HttpRunner {
  async send(req: ApiRequest, timeoutMs: number): Promise<HttpResult> {
    try {
      if (timeoutMs < 1000) {
        return { ok: false, code: 'TIMEOUT', message: '请求超时时间过短', traceId: req.traceId }
      }
      return { ok: true, status: 200, data: '{"code":0,"data":{"name":"demo"}}', traceId: req.traceId }
    } catch (err) {
      return { ok: false, code: 'NETWORK', message: `${err}`, traceId: req.traceId }
    }
  }
}

超时要由网络层控制,页面只消费结果。这样每个页面不会各写一套 loading 和超时逻辑。

5. ResponseParser 解析业务响应

export interface ApiResponse<T> {
  success: boolean
  data?: T
  errorMessage?: string
}

export class ResponseParser {
  parseUserName(result: HttpResult): ApiResponse<string> {
    if (!result.ok) {
      return { success: false, errorMessage: result.message }
    }
    if (result.status < 200 || result.status >= 300) {
      return { success: false, errorMessage: `HTTP 状态异常:${result.status}` }
    }
    const obj = JSON.parse(result.data) as { code: number; data?: { name?: string } }
    return obj.code === 0 && obj.data?.name ? { success: true, data: obj.data.name } : { success: false, errorMessage: '业务响应格式不正确' }
  }
}

解析层要同时检查 HTTP 状态和业务状态,不能只看 200

6. RetryPolicy 判断能不能重试

请添加图片描述

export class RetryPolicy {
  canRetry(method: HttpMethod, result: HttpResult, retryCount: number): boolean {
    if (retryCount >= 2) return false
    if (method !== 'GET') return false
    return !result.ok && (result.code === 'TIMEOUT' || result.code === 'NETWORK')
  }

  delayMs(retryCount: number): number {
    return 1000 * Math.pow(2, retryCount)
  }
}

不是所有请求都能重试。支付、提交订单、创建工单这类请求必须考虑幂等,不能简单重复发送。

7. NetworkAudit 记录排查证据

export interface NetworkAuditRecord {
  traceId: string
  path: string
  method: HttpMethod
  result: 'success' | 'fail'
  costMs: number
  reason?: string
}

export class NetworkAudit {
  private readonly records: NetworkAuditRecord[] = []

  append(record: NetworkAuditRecord): void {
    this.records.push(record)
  }

  recentFailures(): NetworkAuditRecord[] {
    return this.records.filter(item => item.result === 'fail').slice(-20)
  }
}

日志只记录路径、耗时和原因,不记录 Token、手机号、身份证号等敏感字段。

8. HTTP 页面状态映射

export interface NetworkViewState {
  title: string
  action: string
}

export function buildNetworkView(result: HttpResult): NetworkViewState {
  if (result.ok) return { title: '请求成功', action: '继续' }
  if (result.code === 'TIMEOUT') return { title: '网络响应较慢', action: '重试' }
  if (result.code === 'NETWORK') return { title: '网络不可用', action: '检查网络' }
  return { title: '服务暂不可用', action: '稍后再试' }
}

用户看到的是可执行动作,而不是底层异常栈。

9. HTTP 请求验收动作

场景 操作 预期结果
正常请求 访问用户信息接口 返回结构被正确解析
超时 设置短超时 页面展示重试
GET 失败 模拟网络断开 最多重试两次
POST 失败 模拟提交失败 不自动重复提交
敏感日志 检查审计记录 不包含 Token
export function assertApiRequest(req: ApiRequest): void {
  if (!req.traceId || !req.path.startsWith('/')) throw new Error('请求缺少 traceId 或路径非法')
  if (req.method === 'GET' && req.body) throw new Error('GET 请求不应携带 body')
}

10. HTTP 异常排查表

网络问题排查先看 traceId,再看请求阶段。不要只从页面文案判断。

现象 优先查看 处理建议
页面一直转圈 超时时间 所有请求必须有超时
错误文案混乱 错误码映射 网络层统一转换
重复提交 重试策略 非幂等请求不自动重试
服务端说没收到 traceId 客户端服务端串联排查
资源占用 请求销毁 请求完成后关闭会话

HTTP 会话复现场景:给读者一组可执行核验

HTTP 请求要验证超时、解析错误、重试和会话关闭。请求成功一次,不代表弱网和退出页面时安全。

核验维度 读者需要准备的证据
输入 页面入口、用户动作、关键参数
过程 日志、状态变化、异常分支
输出 UI 表现、回调结果、持久化结果
回归 同场景重复执行后的结果
interface HttpReplayCase {
  requestId: any
  timeoutMs: any
  retryCount: any
  closed: any
}

const replay85: HttpReplayCase = {
  requestId: 'sample',
  timeoutMs: 'sample',
  retryCount: 'sample',
  closed: 'sample',
}

function assertReplay85(item: HttpReplayCase): void {
  if (item.retryCount > 0 && !item.closed) throw new Error('重试后会话未关闭')
}

这组核验把 HTTP 请求生命周期补完整,尤其适合检查弱网重试后连接是否被正确关闭。

HTTP 弱网回放表:把文章方法变成可复现动作

HTTP 文章要让读者能跑弱网场景。建议分别模拟连接超时、服务端 5xx、业务码失败和 JSON 解析失败,并观察错误是否进入不同分层。

回放动作 核验方式
连接超时可重试 准备输入、执行操作、记录结果、给出结论
5xx 有退避 准备输入、执行操作、记录结果、给出结论
业务码不重试 准备输入、执行操作、记录结果、给出结论
解析失败走兼容 准备输入、执行操作、记录结果、给出结论

HTTP 链路要按失败阶段分层回放。连接超时适合重试,服务端 5xx 需要退避,业务码失败通常应该展示业务提示,JSON 解析失败要进入兼容或版本处理。读者把这四类失败分别跑一遍,就能判断网络层是否真的稳定,而不是只在成功请求上包装了一层 catch。

HTTP 客户端的落地边界:不要把边界留给读者猜

网络层要避免把所有失败都包装成同一个错误。连接失败、证书失败、状态码失败、业务码失败、解析失败对应不同处理方式。读者落地时应给每类失败保留原始证据,这样排查弱网问题不会只剩一句请求失败。

落地项 处理要求
连接失败看网络 需要有明确输入、处理边界和失败兜底
证书失败看环境 需要有明确输入、处理边界和失败兜底
状态码失败看服务 需要有明确输入、处理边界和失败兜底
业务码失败看协议 需要有明确输入、处理边界和失败兜底

这类边界写清楚后,读者不需要猜哪些逻辑属于页面、哪些属于服务、哪些属于发布前验收。文章的价值也会从“讲了一个功能”变成“给了一套可迁移的工程判断”。

HTTP 联调步骤:按真实路径走一遍

建议额外记录请求生命周期:创建请求、开始发送、收到响应、解析完成、关闭会话。页面退出时如果请求仍在进行,回调必须被丢弃或转入安全状态,不能继续刷新已经销毁的页面。

HTTP 客户端建议准备可控的测试接口。一个接口延迟返回,用于验证超时;一个接口返回 500,用于验证退避;一个接口返回业务错误码,用于验证不重试;一个接口返回畸形 JSON,用于验证解析分支。读者把四个接口都跑通后,再接真实业务接口,网络层会更可靠。

这一步的意义是让读者拿到文章后可以直接复现,而不是只理解概念。技术文章如果能把“输入、动作、日志、结果、失败兜底”写完整,读者照着做时出错概率会低很多。

HTTP 验收补充:补上容易漏掉的边界

HTTP 客户端还应该记录请求关闭时机。页面退出、用户取消、登录态失效、网络切换都会导致请求不再需要继续执行。读者可以分别触发这四种场景,观察连接是否关闭、回调是否被丢弃、UI 是否避免被旧请求覆盖。这样能防止弱网下旧响应覆盖新页面状态。

这类补充不是为了增加篇幅,而是为了让读者在真实项目里少踩坑:正常路径一般最容易跑通,异常路径、退出路径和恢复路径才是质量差距所在。

HTTP 收尾核验:补齐最后一个真实场景

这个场景建议配合请求序号一起做:第一次请求记录为 requestSeq=1,第二次请求记录为 requestSeq=2。当 requestSeq=1 晚于 requestSeq=2 返回时,页面只接受最新序号的结果。这样可以防止弱网环境下旧响应覆盖新状态。

最后再补一个线上回放点:同一个页面连续触发两次查询,第一次请求慢,第二次请求快。验收时要确认第一次慢响应不会覆盖第二次快响应的 UI 状态。这个问题在弱网环境很常见,也是请求治理容易被忽略的细节。

11. 小结:HTTP 层要可追踪

稳定的 HTTP 层不只是发请求,而是统一构造、统一超时、统一解析、统一重试边界和统一审计。页面越少接触底层网络细节,异常越容易定位。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐