应用明明申请了 INTERNET 权限,网络请求还是失败?——一次把 HarmonyOS 网络请求失败原因分层排查清楚

前言

「我在 module.json5 里申请了 ohos.permission.INTERNET,用 @ohos.net.http 发请求,为什么真机上还是失败/没反应?」这是华为开发者论坛里高频出现的提问,也是每一个刚接触 HarmonyOS 网络能力的开发者几乎都会撞上的第一道墙。

它高频,是因为**「申请了网络权限」和「网络请求能成功」之间,隔着不止一层东西**。权限只是第一道闸——后面还有明文流量策略、证书校验、错误码语义、运行时网络环境、设备时钟……任何一层出问题,表现都是「请求失败」,但根因完全不同。更麻烦的是,真机和 Previewer(预览器)行为不一致:Preview 走的是电脑网络栈,不执行手机系统的安全策略,所以「预览器能通、真机不通」几乎是明文 HTTP 被拦截的经典现场,新手很容易反复去改权限,越改越乱。

本文把这个问题按权限层 → 网络配置层 → 错误码层 → 运行时层 → 观测层拆成五层,给出一张「看到某个错误码该往哪查」的映射表,再附一个可运行的分层自检工具。读完你应当能建立一套稳定的排查顺序:先打错误码,再按码分层定位,而不是在权限和地址之间来回试错。篇幅里所有 API 名、错误码、配置字段都尽量给出官方出处;个别随 SDK 版本演化的字段,我会明确标注「以当前 SDK 文档为准」,绝不编造不存在的接口。

为什么这件事值得专门写一篇。 网络请求是几乎每个应用的基础能力,而「权限加了还失败」恰恰处于一个认知盲区:开发者最容易把全部责任归咎于权限,于是在 module.json5 里反复改、反复卸载重装,耗费数小时却毫无进展。事实上,权限在绝大多数网络失败案例里根本不是主因——它只是五层里的第一层,且只要声明正确、装的是新包,就几乎不会出问题。把视野从「权限」拉到「分层」,往往能把排查时间从一下午压缩到十分钟。更有价值的是,这套分层思维不只适用于 HTTP:DNS、TLS、代理、多网卡的问题,在 socket、WebSocket、上传下载等任何网络能力里都是同一套逻辑,学会一次、处处可用。

问题描述

先把现象摆清楚。一个典型的「看起来已经做对了」的现场通常是这样的:

  • 已经在 entry/src/main/module.json5 的 module.requestPermissions 里写了 ohos.permission.INTERNET;
  • 代码用 http.createHttp().request(...) 发起 GET/POST;
  • 在 DevEco 的 Previewer 里能拿到响应;
  • 装到真机(或模拟器)上一点反应都没有,或 catch 里抛出一串看不懂的错误码;
  • 楼主第一反应往往是「是不是权限没生效」,于是一遍遍重装、清缓存,问题依旧。

最小复现条件往往很简单:

  1. 一个只声明了 INTERNET 权限、没有任何网络安全配置的 Stage 工程;
  2. 请求地址是 http:// 开头(明文),或者是一个 https:// 但证书是自签名/内网签发的接口;
  3. 真机运行,把异常吞掉或没有打印 BusinessError.code。

「看起来已经做对了」的配置(但依然失败),归纳起来有这几类,后面会逐层拆解:

  • 权限写在了 app.json5 而不是 module.json5——权限是模块级的,app.json5 里写了不生效;
  • 改了 module.json5 但没 Clean/Rebuild,设备上跑的还是旧包;
  • 请求 http:// 明文地址,被系统安全策略默认拦截;
  • 真机调试本地服务时写 127.0.0.1/localhost,指向的是手机自己而不是电脑;
  • 自签名/内网证书没有配置信任锚,TLS 握手被拒;
  • 代码没有捕获异常,失败时表现就是「无任何响应」。

一个最容易误导人的对照是:同一个地址,用手机浏览器能打开,应用里却失败。这并不能证明「网络是通的、是应用代码的问题」——浏览器和应用走的是不同的证书环境与会话,浏览器手动放行的自签名证书,应用底层网络库并不会自动信任;浏览器走的也可能是系统全局代理,而应用默认走默认网络。所以「浏览器能开」只能排除「这个域名公网不可达」,不能排除证书、明文策略、代理这几层。

下面五层,正好对应上面每一条。排查时请始终带着一个问题:我的请求到底死在了哪一层? 而回答这个问题唯一可靠的方式,是把错误码打出来。

细节解析

第一层:权限层

结论先行:ohos.permission.INTERNET 属于 system_grant(系统授权)的 normal 级别权限,声明正确且装的是新包,安装后即自动授权,不需要任何运行时弹窗。 所以「请求失败」绝大多数情况下不是权限本身没弹窗,而是「声明位置不对」或「跑的还是旧包」。

要同时满足两点才真正生效:

  1. 写在参与构建的那个 module.json5 里,且位于 module.requestPermissions 数组。 权限是模块级声明,app.json5 里写无效。多模块工程尤其要注意:是你真正发包/运行的 module(通常是 entry)的 module.json5,不是别的 module。
  2. 修改配置后必须 Clean / Rebuild,并卸载旧包重新安装。 HarmonyOS 打包时把权限打进安装产物,如果设备上已经装过未带权限的版本,你只是在工程里改了配置而不重新构建安装,设备上运行的仍是旧构建——这是「我明明加了权限怎么还失败」的头号原因。

如何验证权限真的生效,而不是凭感觉?两个 hdc 命令:

# 1) 看安装包里到底打包了哪些权限声明(搜索 ohos.permission.INTERNET)
hdc shell bm dump -n <你的bundleName>

# 2) 看该应用的访问令牌(AccessToken)里该权限的授予状态
hdc shell atm dump -t -b <你的bundleName>
  • bm dump -n <bundleName> 输出的是已安装包的清单信息,在其中搜索 ohos.permission.INTERNET,确认它出现在 reqPermissions(声明权限)列表中——这一步验证「包里确实带了这个权限」。
  • atm(AccessTokenManager)dump -t -b <bundleName> 输出访问令牌的权限状态表 permStateList,确认 ohos.permission.INTERNET 存在且 grantState 为已授予(通常值为 1)。这一步验证「系统确实把它授予了你的应用」。

注意:不同 SDK 版本 bm / atm 输出的字段名和 flag 可能有差异(例如 atm 的精确子命令、JSON 字段),执行后以当前 hdc 实际输出结构为准;只要能在输出里确认 ohos.permission.INTERNET 被声明且被授予,即可排除权限层。

如果日志里出现 ohos.permission.INTERNET check failed,或捕获到错误码 201(Permission denied),那就是权限层没过,回头查上面两点。

澄清一个常见误解:INTERNET 不需要 ACL,也不要去 ACL 后台找它。 HarmonyOS 的权限按敏感程度分 normal / system_grant / user_grant,而 user_grant 中更敏感的一批(如位置、通讯录、麦克风等)以及少数受限的 system_grant 权限,才需要在 AppGallery Connect 申请 ACL(AccessToken 白名单/受限权限审批)后才能发布使用。ohos.permission.INTERNET 是 normal 级 system_grant,不涉及 ACL、不需要运行时弹窗、也不需要在后台提交任何申请——只要在 module.json5 声明且装新包即可。新手看到「权限」二字容易联想到 ACL 审批流程,在后台反复提交,纯属走错方向。

验证权限的两个层次要分开看。 bm dump 看的是「包里声明了什么」,atm dump 看的是「系统授予了什么」。两者都确认才算权限真正生效;只确认前者(包里有声明)而后者因某种原因未授予,请求仍会被 201 拒绝。极少数情况下,旧包残留、多用户/多设备账号、或覆盖了相同 bundleName 的签名冲突,可能导致「声明在、授权状态却异常」,这时的标准解法依然是:彻底卸载 → Clean/Rebuild → 重新安装,让 atm 重新下发令牌。

额外提醒:如果应用是元服务(Atomic Service),还有一层域名白名单约束——未备案/未配置的域名会被 2300998(It is not allowed to access this domain) 拦截,这与普通应用的权限无关,需到元服务后台配置服务器域名。

第二层:网络配置层

权限过了,请求仍失败,第二常见原因在网络安全配置,尤其是明文 HTTP 与证书信任。

(1)明文 HTTP 被系统安全策略拦截。 HarmonyOS 默认只允许 HTTPS 流量,直接请求 http:// 开头的明文地址会被拦截,典型错误码是 2300997(Cleartext traffic not permitted)。这是「真机失败、Previewer 正常」的最典型原因:Previewer 走电脑网络栈、不执行手机安全策略,真机会严格拦截明文流量。

开发调试阶段若必须访问 HTTP(本地/内网服务),需要在 src/main/resources/base/profile/ 下建 network_config.json,并在 module.json5 里用 networkSecurityConfig 引用它。该配置仅限调试,上线前应改回禁止明文,或确保正式接口全部 HTTPS。

字段名随 SDK 版本有演化:当前(API 18+)profile 文件里字段是 cleartextTrafficPermitted(位于 network-security-config.base-config 下);早期文档里 module.json5 内联写法用过 cleartextPermitted。具体字段以当前 SDK 文档为准,本文 示例代码 一节给的是当前推荐写法。

(2)自签名 / 内网证书需要配置信任锚。 https:// 接口若用的是自签名证书、内网 CA 或证书链不完整,真机会拒绝 TLS 握手,常见错误码 2300060(Invalid SSL peer certificate)、2300077(SSL CA certificate does not exist or is inaccessible)。浏览器能手动放行,不等于应用请求会放行——应用走系统证书校验,不会继承你在浏览器里点过的「继续访问」。修复方式有两种:首选是把服务端换成受信任 CA 签发的证书;若确实必须用自签名/内网 CA(如内网调试、企业中间件),则在 network_config.json 的 base-config.trust-anchors 里追加自定义信任锚,把你的 CA 证书作为 raw 资源放进 resources 后引用。注意信任锚是追加到系统内置 CA 之上,而不是替换,所以不会影响其他正常 HTTPS 站点。

证书链不完整是另一类高频坑:服务端只下了叶子证书、漏了中间 CA,浏览器因自带中间证书缓存能连,应用却因链校验失败报 2300060。这类问题在服务端补全证书链即可,不需要在客户端动信任锚。

(3)证书固定(certificate pinning)。 若服务端或中间网关启用了证书固定(SPKI pinning),而客户端实际拿到的证书与固定值不匹配,也会在 TLS 层失败。这类属于安全策略冲突,应从证书来源/固定配置上解决,不建议为排查而关闭校验——关闭证书校验等于把明文流量和中间人攻击的大门同时打开,只能作为「验证是不是证书问题」的一次性临时手段,绝不能留在正式包里。

(4)配置优先级要记牢。 明文策略的生效优先级是 component-config(按组件,如 Request / Network Kit / ArkWeb)> domain-config(按域名)> base-config(全局)。也就是说,即便全局 base-config.cleartextTrafficPermitted 设了 true,只要某个域名的 domain-config 显式写了 false,该域名仍禁止明文。调试时若「全局开了明文还不生效」,先检查是不是被更具体的域名/组件规则覆盖了。

第三层:错误码层

@ohos.net.http 失败时,回调/异常里的 BusinessError.code 是最有价值的线索。23 开头的整段对应底层网络库(curl 家族)的错误,尾号才是关键。下面是一张「看到这个码,第一时间往哪查」的映射表——建议直接抄进你的排查工具:

怎么把码打出来。 用 Promise 写法就在 catch((err) => { const e = err as BusinessError; console.error(e.code, e.message); });用回调写法就在 err 分支里同样打印。BusinessError 除了 code 还带 message,务必把整个对象 JSON.stringify 打全——证书类错误常在 message 里直接写明是 hostname mismatch 还是 certificate has expired,能省掉一轮猜测。另一个极易混淆的点:HTTP 的 401/403/404/500 等是服务端通过响应返回的业务状态码,取自在 then 里的 response.responseCode,它们和上面「请求异常码」完全是两回事——前者说明「请求成功发出且服务端回了话」,后者说明「请求根本没成功」。把两者混为一谈,会让你在「服务端没问题」和「客户端没连上」之间反复横跳。

几个高频组合,直接照着查:

  • 201 → 权限层,且大概率是「旧包」或「写错位置」,不是 ACL。
  • 2300997 → 明文被拦,立刻去查 network_config.json,不要再去动权限。
  • 2300006 后紧跟 2300007 → DNS 过了但 TCP 不通,重点查端口/防火墙/网段,而不是域名。
  • 2300060 + 设备时间异常 → 先校正系统时间,再怀疑证书本身。
  • 2300028 且浏览器同地址正常 → 多半是服务端响应慢或连接被中间设备复位,调大 readTimeout 并抓包看 ServerHello。
错误码官方语义第一时间怀疑该往哪查
201Permission denied没声明/没授予 INTERNETmodule.json5 的 requestPermissions 与安装态(bm/atm dump)
2300001Unsupported protocol协议不支持(如写了 ftp://)请求 URL 的 scheme
2300003Invalid URL format or missing URLURL 格式错误或缺 URLrequest 的 url 参数
2300005Failed to resolve the proxy name代理名解析失败(配了代理)应用代理配置
2300006Failed to resolve the host nameDNS 解析失败域名拼写、DNS 配置、内网 DNS、VPN/代理
2300007Failed to connect to the serverTCP 连不上IP/端口、防火墙、服务是否监听、网段是否可达
2300008Invalid server response服务端回复异常服务端实现
2300028Operation timeout连接/读取超时弱网、服务端响应慢、connectTimeout/readTimeout 设置
2300052The server returned nothing服务端空响应(无 header/data)服务端是否真的返回了内容
2300058Local SSL certificate error本地证书错误客户端证书/CA 路径
2300060Invalid SSL peer certificate or SSH remote key对端证书无法认证域名匹配、有效期、证书链、自签名
2300077SSL CA certificate does not exist or is inaccessibleCA 证书找不到/不可访问trust-anchors 配置与资源路径
2300997Cleartext traffic not permitted明文 HTTP 被拦network_config.json 的 cleartextTrafficPermitted
2300998It is not allowed to access this domain不允许访问该域名(元服务/安全策略)域名白名单/安全策略
2300999Internal error内部错误结合 hilog 进一步定位

说明:2300997 / 2300998 / 2300999 在较新 SDK 中才有明确语义;2300058/2300060/2300077 属 TLS/证书类。BusinessError 除了 code 还带 message,排查时建议把整个对象 JSON.stringify 打全,证书类错误常会在 message 里直接写明是 hostname mismatch 还是 certificate has expired,能省掉一轮猜测。HTTP 401/403/404/500 等是服务端返回的业务状态码,走 response.responseCode,不要和上面的请求异常码混为一谈。

第四层:运行时层

权限、配置、错误码都查完还没定位,就要看请求运行时的网络环境。

  • DNS / 代理 / VPN / 多网卡。 手机「能上网」不代表你的接口可达:连着公司 WiFi 但目标服务在内网、走了特殊 DNS,或开了 VPN 改变了出口,都可能让某个域名解析不到正确 IP。可先用浏览器在手机上开同一地址做对照,但注意浏览器和应用不一定共享同一套证书/会话环境。

  • 多网卡下的默认网络。 用 connection.getDefaultNetSync() 可以拿到系统默认网络的句柄(NetHandle),其 netId 为 0 表示当前无可用默认网络。该接口需要 ohos.permission.GET_NETWORK_INFO(normal 级,同样要在 module.json5 声明)。若应用需要走特定网络(如蜂窝而非 WiFi、或某 VPN),应通过 connection.createNetConnection 绑定网络或 setAppHttpProxy 设置代理,而不是假设默认网一定可达目标。特别地,setAppHttpProxy 只是配置代理规则、不校验代理服务有效性,且必须在 HttpRequestOptions 里把 usingProxy 设为 true 才会真正走代理——漏掉这个开关,配了代理也不生效。

  • 单次请求的数据体量限制。 @ohos.net.http 的 request 接口仅支持接收 5MB 以内的数据。若你的接口返回超过 5MB(例如大文件下载、整包报表),request 会失败或截断,这类场景应改用上传下载任务(request.agent / @ohos.request)或分片拉取,而不是硬扛在 http 模块里。下载场景若提示 2300023(Failed to write received data to disk/application),先看是不是文件超过 5MB、或应用没有写文件权限。

  • 请求发在生命周期过早的时机。 冷启动时如果一进 UIAbility 或页面 aboutToAppear 就立刻发请求,而此时数据网络尚未真正就绪,可能拿到连接类错误。更稳妥的做法是:先通过 connection.getDefaultNetSync().netId !== 0 确认有默认网络,或监听 connection.on('netAvailable') 后再发起;后台切回前台后旧连接可能已被系统回收,需要重建请求对象。

  • 同步阻塞误区。 ArkTS 主线程没有「同步 HTTP」可用,所有请求都是异步的。不要写 while(!done){} 这类忙等去等结果——会冻结 UI 甚至触发异常。用 async/await 或 Promise 正确处理异步结果。

  • 设备时钟不同步。 手机时间若明显快于或慢于真实时间,TLS 证书会处于「尚未生效」或「已过期」区间,表现为 2300060 这类证书错误。先在设置里把时间校正为自动同步,再去怀疑证书本身。

  • WiFi/蜂窝切换后连接挂起。 从 WiFi 切到蜂窝后,@ohos.net.http 可能复用旧的僵死 TCP 连接,导致请求一直挂起。可临时给请求头加 Connection: close 验证,或监听网络变化后重建 httpClient(createHttp() 新对象)。

  • 「无任何响应」最常见的真相:异常被吞。 大量「请求没反应」的帖子,根因不是网络,而是代码根本没处理失败分支——request(...) 抛错后没有 catch,或 catch 里只写了空 {},表现就是 UI 一动不动、日志空空。另一个变体是「请求还没结束就调了 destroy()」,对象被提前释放,回调永远不触发。排这类问题不需要任何网络知识:在调用前、成功分支、失败分支各打一行日志,再设合理的 connectTimeout/readTimeout,真相立刻显现。这一步永远应该排在「怀疑网络」之前。

第五层:观测层

当以上都靠「猜」时,就该上日志和抓包了。

hilog 过滤网络子系统。 通过 hdc 抓日志,重点看网络栈(NetStack)相关输出:

# 实时抓全部日志,按关键字过滤(网络栈 / http / ssl 相关)
hdc shell hilog | grep -iE "netstack|http|ssl|curl"

# 只抓 NetStack 标签(标签名以当前系统为准,常见为 NetStack / NETSTACK)
hdc shell hilog -T NetStack

# 清屏后再复现,避免历史日志干扰
hdc shell hilog -r
hdc shell hilog -T NetStack

注意:hilog 的 tag 命名与缓冲区分级随系统版本可能不同,命令中的 -T 标签名、grep 关键字以当前设备实际输出为准。怀疑证书类问题时,把日志级别调到 debug/info 再复现,往往能看到更细的 TLS 握手信息。

日志缓冲与落盘的小技巧。 默认 hilog 环形缓冲有限,长时间运行后早期日志会被覆盖。建议复现前先 hdc shell hilog -r 清缓冲,再立刻复现、立刻抓取,避免关键日志被冲掉。如果要长期留存,可用 hdc shell hilog > net.log 重定向到本地文件,或用 hdc shell hilog -w 持续写文件。对于网络栈,重点关注的不仅是错误码,还有「ClientHello 有没有发出去」「TCP 连接有没有 establish」这类阶段信息——如果 hilog 里连 ClientHello 都看不到,说明请求根本没走到网络库(多半是 URL/参数/权限问题);如果看到了 ClientHello 但停在 TLS 阶段,那基本锁定是证书层。

DevEco Studio 的观测入口。 DevEco 提供了 Profiler(性能分析)中的网络相关分析能力,可观察请求耗时、连接建立等;需要抓包级细节时,可结合 hdc 导出或借助 ArkWeb 的网络面板(仅对 Web 组件内的请求)。具体菜单入口随 DevEco 版本可能调整,以当前 IDE 版本为准——核心思路是:先用 hilog 拿到错误码与 TLS 细节,再用 Profiler 看「请求到底有没有发出去、卡在哪一段」。

示例代码

module.json5:权限声明与网络安全配置

下面是正确的 module.json5 片段。权限写在 module.requestPermissions,并通过 networkSecurityConfig 引用 profile 里的明文/证书策略。ohos.permission.GET_NETWORK_INFO 是为后面自检工具里的 DNS/默认网络探测准备的,按需声明。

{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        // 网络权限:system_grant,声明正确且装新包即自动授权,无需运行时弹窗
        "name": "ohos.permission.INTERNET"
        // reason / usedScene 对 system_grant 为可选;user_grant 才必须
      },
      {
        // 自检工具里用 connection 探测默认网络/DNS 时需要,normal 级
        "name": "ohos.permission.GET_NETWORK_INFO"
      }
    ],
    // 引用 resources/base/profile/network_config.json 里的网络安全配置
    "networkSecurityConfig": {
      "name": "$profile:network_config"
    }
  }
}

networkSecurityConfig.name 的资源引用写法(是否带 $profile: 前缀)随 SDK 版本可能不同,部分版本直接写文件名字符串 "network_config";以当前 SDK 文档为准。

network_config.json:明文放行与信任锚示例

放在 entry/src/main/resources/base/profile/network_config.json。调试期放开明文,正式环境务必改回 false 或仅对必要域名放行。

{
  "network-security-config": {
    "base-config": {
      // 调试期允许明文 HTTP(仅调试!上线前改回 false 或删掉)
      "cleartextTrafficPermitted": true,
      // 默认信任系统内置 CA;自签名/内网证书在此追加信任锚
      "trust-anchors": [
        { "certificates": "/etc/security/certificates" }
      ]
    },
    "domain-config": [
      {
        "domains": [
          { "include-subdomains": true, "name": "api.example.com" }
        ],
        // 对正式域名强制 HTTPS,即便 base-config 放开明文也不影响它
        "cleartextTrafficPermitted": false
      }
    ]
  }
}

自签名 CA 的信任锚通常写成 trust-anchors: [{ "source": { "rawResource": "my_ca" } }],引用放入 resources 的证书资源;具体字段以当前 SDK 文档为准。配置优先级:component-config > domain-config > base-config。

分层自检工具(ArkTS)

下面这个工具依次探 DNS → TCP 连通 → TLS/HTTP,逐层输出结论,定位「到底卡在哪一层」。注意 socket 的当前接口名是 constructTCPSocketInstance()(旧版曾叫 constructTCPSocket);connection.getAddressesByName 需要 GET_NETWORK_INFO 权限。

import http from '@ohos.net.http';
import connection from '@ohos.net.connection';
import socket from '@ohos.net.socket';
import { BusinessError } from '@kit.BasicServicesKit';

interface LayerResult {
  layer: string;
  ok: boolean;
  detail: string;
}

// 第一层:DNS 解析。需要 ohos.permission.GET_NETWORK_INFO
async function probeDns(host: string): Promise<LayerResult> {
  try {
    const addrs = await connection.getAddressesByName(host);
    // 不同 SDK 返回 string[] 或 NetAddress[],统一取地址串
    const list: string[] = (Array.isArray(addrs) ? addrs : [])
      .map((a) => (typeof a === 'string' ? a : (a as connection.NetAddress).address))
      .filter((x): x is string => !!x);
    if (list.length === 0) {
      return { layer: 'DNS', ok: false, detail: '解析到 0 个 IP' };
    }
    return { layer: 'DNS', ok: true, detail: '解析成功: ' + list.join(', ') };
  } catch (e) {
    const err = e as BusinessError;
    return { layer: 'DNS', ok: false, detail: `解析失败 code=${err.code} msg=${err.message}` };
  }
}

// 第二层:TCP 连通性(用原生 socket 直连,独立于 HTTP 层)
function probeTcp(ip: string, port: number, timeoutMs = 3000): Promise<LayerResult> {
  return new Promise((resolve) => {
    let tcp: socket.TCPSocket;
    try {
      // 注意:当前接口名是 constructTCPSocketInstance(),旧版曾为 constructTCPSocket
      tcp = socket.constructTCPSocketInstance();
    } catch (e) {
      resolve({ layer: 'TCP', ok: false, detail: '创建 socket 失败: ' + (e as BusinessError).message });
      return;
    }
    const opt: socket.TCPConnectOptions = { address: { address: ip, port }, timeout: timeoutMs };
    let done = false;
    const finish = (ok: boolean, detail: string) => {
      if (done) { return; }
      done = true;
      try { tcp.close(); } catch (_) { /* 忽略关闭异常 */ }
      resolve({ layer: 'TCP', ok, detail });
    };
    tcp.connect(opt)
      .then(() => finish(true, `TCP 连通 ${ip}:${port}`))
      .catch((err: BusinessError) => finish(false, `TCP 失败 code=${err.code} msg=${err.message}`));
    // 安全兜底:防止 timeout 选项未触发时永久挂起
    setTimeout(() => finish(false, `TCP 超时(${timeoutMs}ms) ${ip}:${port}`), timeoutMs + 1000);
  });
}

// 第三/四层:HTTPS 请求;证书类错误归因到 TLS 层,其余归因到 HTTP 层
const CERT_CODES = [2300058, 2300059, 2300060, 2300061, 2300077];
async function probeHttp(url: string): Promise<LayerResult> {
  const req = http.createHttp();
  try {
    const resp = await req.request(url, {
      method: http.RequestMethod.GET,
      connectTimeout: 8000,
      readTimeout: 8000
    });
    return { layer: 'HTTP', ok: true, detail: `响应码 ${resp.responseCode}` };
  } catch (e) {
    const err = e as BusinessError;
    const layer = CERT_CODES.includes(err.code) ? 'TLS' : 'HTTP';
    return { layer, ok: false, detail: `code=${err.code} msg=${err.message}` };
  } finally {
    req.destroy();
  }
}

// 串联四层,逐层打印结论
async function selfCheck(host: string, httpsUrl: string, port: number = 443): Promise<void> {
  const dns = await probeDns(host);
  console.info(`[${dns.layer}] ${dns.ok ? 'OK' : 'FAIL'} - ${dns.detail}`);

  let ip = '';
  if (dns.ok) {
    // 取第一个解析到的 IP 做 TCP 探测(示意,真实场景可按需逐个探)
    ip = dns.detail.replace('解析成功: ', '').split(',')[0]?.trim() ?? '';
  }
  if (ip) {
    const tcp = await probeTcp(ip, port);
    console.info(`[${tcp.layer}] ${tcp.ok ? 'OK' : 'FAIL'} - ${tcp.detail}`);
  } else {
    console.info('[TCP] SKIP - DNS 未解析到可用 IP');
  }

  const httpR = await probeHttp(httpsUrl);
  console.info(`[${httpR.layer}] ${httpR.ok ? 'OK' : 'FAIL'} - ${httpR.detail}`);
}

// 用法示例:先在真机调试时替换为你自己的域名与 HTTPS 地址
// selfCheck('api.example.com', 'https://api.example.com/health');

一个正确的最小请求示例

把异常完整打印出来,是所有排查的起点——没有任何响应,往往只是异常被吞掉了。

import http from '@ohos.net.http';
import { BusinessError } from '@kit.BasicServicesKit';

async function doRequest(): Promise<void> {
  // 每次请求创建独立的 HttpRequest 对象
  const req = http.createHttp();
  try {
    const response = await req.request(
      // 真机调试本地服务时,用电脑局域网真实 IP,不要写 localhost/127.0.0.1
      'https://www.example.com/api/test',
      {
        method: http.RequestMethod.GET,
        header: { 'Content-Type': 'application/json' },
        connectTimeout: 10000,
        readTimeout: 10000,
        expectDataType: http.HttpDataType.STRING
      }
    );
    console.info(`响应码: ${response.responseCode}`);
  } catch (err) {
    // 必须捕获异常:错误码是定位问题的关键依据
    const e = err as BusinessError;
    console.error(`请求失败 code: ${e.code}, message: ${e.message}`);
  } finally {
    // 完成后销毁对象,释放资源,避免内存泄漏
    req.destroy();
  }
}

总结

排查顺序清单(Checklist)

  1. 先打错误码:在 catch 里打印 BusinessError.code 和完整 message,这是分层的唯一可靠入口。
  2. 见到 201 / ohos.permission.INTERNET check failed → 查权限层:module.json5 的 requestPermissions 是否写了、是否写在 module 而非 app、是否 Clean/Rebuild 并重装了新包;用 bm dump / atm dump 确认声明与授予。
  3. 见到 2300997 → 明文 HTTP 被拦:配 network_config.json 的 cleartextTrafficPermitted(仅调试),或改用 HTTPS。
  4. 见到 2300006 → DNS 层:换 IP 直连排除 DNS;查域名拼写、内网 DNS、VPN。
  5. 见到 2300007 → TCP 层:用 nc/telnet 或本文 TCP 探测验证端口通不通;查防火墙、服务监听、网段。
  6. 见到 2300060 / 2300077 / 2300058 → TLS/证书层:查域名匹配、有效期、证书链、信任锚;先校正设备时间。
  7. 见到 2300028 / 2300052 → 超时/空响应层:查弱网、服务端响应、超时参数设置。
  8. 「无任何响应」 → 八成是异常被吞,或请求根本没执行/未完成就 destroy();在调用前、成功、失败三处都加日志。
  9. 真机调本地服务 → 用电脑局域网 IP(不要用 127.0.0.1/localhost),确认服务监听 0.0.0.0、防火墙放行。
  10. 仍无头绪 → hdc shell hilog 过滤网络栈,带脱敏错误码、请求协议(HTTP/HTTPS)、SDK 版本、目标公网/局域网信息进一步定位。

最容易踩的坑

  1. 权限写在 app.json5:权限是模块级,必须写运行模块的 module.json5 的 requestPermissions;app.json5 里写了不生效。
  2. 改了配置没重装:只改工程不 Clean/Rebuild 并卸载重装,设备上还是旧包——「加了权限仍失败」头号原因。
  3. 真机写 127.0.0.1/localhost:那指向手机自己,不是开发电脑;要用电脑局域网真实 IP,服务监听 0.0.0.0。
  4. 把服务端 4xx/5xx 当成「请求失败」:401/403/404/500 是服务端回了话的业务状态码(在 response.responseCode 里,走 then 分支),和 23 开头的请求异常码完全是两码事。混为一谈会让你在「服务端没问题」和「客户端没连上」之间反复横跳。
  5. catch 里只写 {} 或只打 err.message:最有价值的 BusinessError.code 反而没打出来。请务必打印完整对象(JSON.stringify(err)),证书类问题常常直接写在 message 里。
  6. socket 接口名:当前 TCP socket 构造接口是 socket.constructTCPSocketInstance(),旧版才是 constructTCPSocket,照抄旧文档容易编译失败。
  7. 为排查而关闭证书校验:证书错误应修证书或正确配信任锚,不建议为「先跑通」而长期关闭校验,正式环境会留下安全隐患。

参考链接

  • HarmonyOS 网络连接安全配置(networkSecurityConfig / cleartextTrafficPermitted):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/network-security-config-overview
  • @ohos.net.http 数据请求与错误码:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-http-V5
  • @ohos.net.connection 网络连接管理(getDefaultNetSync / getAddressesByName):https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-net-connection
  • @ohos.net.socket Socket 连接(constructTCPSocketInstance / connect):https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V14/js-apis-socket-V14
Logo

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

更多推荐