网络检测页出现一个红色的“失败”提示,看似已经结束了这次检测,实际上却把最有用的信息藏起来了。用户看到 HTTP 503 与开发者看到 2300006,两者的排查入口完全不同:前者意味着收到了服务器的 HTTP 响应,后者则指向域名解析阶段。把所有情况都统一写成“请检查网络”,会让一个诊断工具在关键时刻失去意义。

“链路镜”系列前两篇已经固定了检测入口 https://example.com/、NetworkProbePage.ets 页面和 idle/loading/ok/error/cancelled 五种状态,并加入 currentId 防止取消之后的旧回调覆盖新状态,inFlight 上限为 2。第三篇仍然使用同一个请求组件,只在检测结果解释这一层动手:区分 HTTP 状态、NetworkKit 错误码和用户主动取消,再为每类失败给出可执行的检查建议。这种做法不需要额外的系统权限,也不会擅自把一次失败判定成“设备离线”。

网络故障分层诊断的技术封面

一、先拆开三个经常混在一起的概念

一个 HTTPS GET 请求会经历 DNS 查询、连接建立、TLS 协商、发送请求和接收 HTTP 响应等步骤。只有服务器真的返回了响应头,才有 HTTP 状态码可讨论。比如 401 代表请求缺少有效身份凭据,404 表示目标资源不存在,429 则是服务端提示请求过于频繁;这些值不代表设备的 Wi-Fi 一定有问题。500 或 503 同样应该优先关注服务端,而不是建议用户不断开关飞行模式。

另一条分支来自 request() 抛出的异常。华为 NetworkKit HTTP API 将一组 cURL 映射错误定义为 2300000 + cURL 错误号,其中 2300006 是主机名解析失败、2300007 是连接失败、2300028 是操作超时、2300060 涉及服务器证书或 SSH 远端密钥校验。它们是传输或环境层的现象,不是 HTTP 响应状态。即便同为超时,也可能由 DNS、链路、服务器响应过慢等不同因素引起;前端页面最多给出排查方向,不应编造具体根因。

第三条分支是取消。上一篇的 cancelProbe() 会递增 currentId 并让旧 Promise 失去提交 UI 的资格,但不会把底层传输自动解释为已中断。它是一条用户交互结果,不该落进“请求错误码”分类。把这三条分支分开,错误提示才有可信的语义基础。

二、把诊断判断从页面逻辑中抽离

上篇的 startProbe() 使用 http.createHttp() 创建单次请求,在 finally 释放资源,保留了 connectTimeout: 8000 与 readTimeout: 8000。我们新增 common/ProbeAdvice.ets,纯函数只接收数字并返回中文标题与排查动作,不读取组件状态。这样做的好处是即使没有真实网络环境,也可以对输入分类做确定性单元检查。

// entry/src/main/ets/common/ProbeAdvice.ets
export interface ProbeAdvice {
  category: string;
  title: string;
  action: string;
}

export function describeHttp(code: number): ProbeAdvice {
  if (code >= 200 && code < 300) {
    return { category: 'http', title: `目标可访问 · HTTP ${code}`,
      action: '本次目标请求得到成功响应' };
  }
  if (code === 401 || code === 403) {
    return { category: 'http', title: `访问受限 · HTTP ${code}`,
      action: '核对身份凭据与服务端访问权限' };
  }
  if (code === 404) {
    return { category: 'http', title: '资源不存在 · HTTP 404',
      action: '检查请求路径和资源是否已迁移' };
  }
  if (code === 429) {
    return { category: 'http', title: '请求频率受限 · HTTP 429',
      action: '按服务端限流规则稍后重试' };
  }
  if (code >= 500 && code < 600) {
    return { category: 'http', title: `服务端异常 · HTTP ${code}`,
      action: '检查服务状态与服务端日志' };
  }
  return { category: 'http', title: `HTTP ${code}`,
    action: '核对该状态码对应的资源与重定向策略' };
}

export function describeTransport(code: number): ProbeAdvice {
  switch (code) {
    case 2300006:
      return { category: 'transport', title: '域名解析失败',
        action: '检查域名拼写、DNS 配置和当前网络' };
    case 2300007:
      return { category: 'transport', title: '目标连接失败',
        action: '检查目标端口、防火墙和服务监听状态' };
    case 2300028:
      return { category: 'transport', title: '请求超时',
        action: '结合连接阶段和服务端处理时间进一步定位' };
    case 2300060:
      return { category: 'transport', title: '证书验证异常',
        action: '核对证书链、域名匹配和设备时间' };
    default:
      return { category: 'transport', title: `网络请求异常 · ${code}`,
        action: '保存错误码,按官方 HTTP 错误表继续排查' };
  }
}

这里的分类表故意没有把所有错误号塞进一个巨大的 switch。2300003 可能是 URL 格式错误,201 可能与权限有关,其他未知码也需要保留原值。一个能解释五种已知异常、同时不掩盖未知异常的工具,比一个看似覆盖全部却充满猜测的诊断表更可靠。正式产品可以逐步增加映射,但每增加一条都应核查官方含义。

DevEco Studio 白色主题下的诊断映射与页面预览

三、保留旧请求隔离,再接入诊断结果

在 NetworkProbePage.ets 中引入这两个函数,原有的 phase、lastCode、currentId 与 inFlight 不变,只新加一条 @State private action: string = ''。下面展示可以替换上篇请求方法的完整关键实现;cancelProbe() 仍采用此前的请求代次逻辑。页面中的两行 Text(this.detail) 与 Text(this.action) 分别承担“现象”和“下一步动作”,不用额外弹出系统权限对话框。

import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { describeHttp, describeTransport, ProbeAdvice } from '../common/ProbeAdvice';

private async startProbe(): Promise<void> {
  if (this.phase === 'loading') { return; }
  if (this.inFlight >= 2) {
    this.detail = '旧请求尚未完成清理';
    this.action = '稍后再发起检测';
    return;
  }
  const id: number = ++this.currentId;
  this.inFlight += 1;
  this.phase = 'loading';
  this.detail = '正在检测 HTTPS 目标';
  this.action = '';
  this.lastCode = 0;
  const request = http.createHttp();
  try {
    const response = await request.request('https://example.com/', {
      method: http.RequestMethod.GET,
      connectTimeout: 8000,
      readTimeout: 8000
    });
    if (id !== this.currentId) { return; }
    const advice: ProbeAdvice = describeHttp(response.responseCode);
    this.lastCode = response.responseCode;
    this.phase = response.responseCode >= 200 && response.responseCode < 300
      ? 'ok' : 'error';
    this.detail = advice.title;
    this.action = advice.action;
  } catch (error) {
    if (id !== this.currentId) { return; }
    const err = error as BusinessError;
    const advice: ProbeAdvice = describeTransport(err.code);
    this.phase = 'error';
    this.lastCode = 0;
    this.detail = advice.title;
    this.action = advice.action;
  } finally {
    request.destroy();
    this.inFlight -= 1;
  }
}

这段代码有两个需要刻意检查的细节。第一,HTTP 响应即使状态是 503,也是 try 分支中的正常收到响应,不能直接写成抛出网络异常。第二,所有结果在更新 UI 前都先判断 id !== this.currentId,包括 catch 分支。否则用户取消后,网络层恰好产生一次超时,就会把原本的“已取消”覆盖为“请求超时”。

finally 必须继续保留,因为成功、失败、旧结果忽略都要销毁本次 HttpRequest。注意不同版本上 destroy() 的实际释放与请求完成时机应以平台文档和实测为准,不能将它当作取消业务状态的唯一机制。只有最外层的代次判断,才能明确决定哪些返回仍拥有页面写入权。要让取消时提示稳定,可以在原 cancelProbe() 中同步把 action 改成“重新点击开始检测即可”,并清掉 lastCode。

四、排查时不要直接把“异常码”等于“原因”

对于 2300006,第一步检查目标域名是否拼错,接着在设备可控网络下确认 DNS 是否能解析,不要一上来就重装应用。2300007 更应该检查服务端监听、端口和防火墙;若同一设备能访问其他 HTTPS 站点,证据将倾向目标侧,但依然不能据此百分之百认定服务器故障。2300060 应核查证书链、主机名是否匹配、有效期以及设备时间,不建议通过无条件跳过证书校验来掩盖问题。

HTTP 401/403 的排查方向是身份与授权,404 是路径,429 是限流,5xx 则偏向服务端。真实线上服务可以同时存在多个故障,浏览器访问与应用访问也可能走不同代理,所以“重现步骤 + 错误阶段 + 原始错误码 + 当前配置”要一起记录。针对固定的 https://example.com/,如果得到 200,我们只能得出“这次目标请求得到成功响应”,不能说整台设备的网络毫无问题,更不能自动扩展为所有域名都可用。

HTTP 状态与传输错误分流排查图

需要额外防范一个“看起来很专业”的错误提示:直接把某个系统错误码翻译成唯一的业务结论。例如,连接失败不等于用户手机一定断网,证书错误也不等于网站遭到攻击。错误码更像沿途留下一张检查站收据,告诉我们在哪一阶段停止,但未必记录了导致失败的全部条件。页面应保留原始值,避免把尚未获得的网络证据写成肯定判断。对于 429,如果服务端提供了可解释的重试提示,可以在遵守其策略的前提下告知用户;不能将所有失败统一安排为每秒自动重试,否则可能进一步放大目标服务的压力。

错误建议的语言也有设计边界。“检查服务器日志”是给开发或运维人员的建议,普通用户界面可以改成“服务暂不可用,可稍后重试”,但底层保留同一 category 与错误编号。把面向用户的提示与技术诊断信息拆成两个展示层,既减少焦虑,也不会损失必要的定位线索。本文使用同一页展示详细建议,是为了突出排查路径;正式上架时还要根据受众决定是否默认展开这些信息。

五、把可复现验证做成一张对照清单

不用制造伪造的 HiLog 结果,也能先把分类函数检查清楚。在独立的单元测试或调试入口中,逐个调用 describeHttp(404)、describeHttp(503)、describeTransport(2300006)、describeTransport(2300028),核对返回的 category、title 和 action。这些是纯函数输入输出检查,不会向真实网络发包。接下来再通过实际 DevEco 调试和可控测试服务器验证 UI 的请求顺序:点击检测、立刻取消、再次检测,确保旧请求无论成功还是报错,都不反向修改页面。

特别需要单独设计证书异常、连接超时和 HTTP 503 的可控测试条件,不能假定公网示例网站会恰好返回预期故障。测试结束时记录系统版本、NetworkKit 实际响应、耗时测量方式与网络条件;在得到真实日志之前,流程图和页面示意只能用于阐述代码路径。module.json5 仍需声明 ohos.permission.INTERNET,因为这里没有改变 NetworkKit 发起网络请求的基本授权条件。

至此,“链路镜”已从一次简单 HTTPS 检测,逐渐变成能区分“用户撤销、HTTP 响应、传输异常”的小型诊断页面。下一篇会把这些字段收敛成结构化的最近检测记录,解决可追踪与敏感信息边界问题;但原始 HTTP 响应体和用户凭据不应为了方便排查就无差别落盘。

官方参考:HTTP 请求 API 与错误码。

Logo

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

更多推荐