应用明明申请了 INTERNET 权限,网络请求还是失败?——一次把 HarmonyOS 网络请求失败原因分层排查清楚
应用明明申请了 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里抛出一串看不懂的错误码; - 楼主第一反应往往是「是不是权限没生效」,于是一遍遍重装、清缓存,问题依旧。
最小复现条件往往很简单:
- 一个只声明了
INTERNET权限、没有任何网络安全配置的 Stage 工程; - 请求地址是
http://开头(明文),或者是一个https://但证书是自签名/内网签发的接口; - 真机运行,把异常吞掉或没有打印
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 级别权限,声明正确且装的是新包,安装后即自动授权,不需要任何运行时弹窗。 所以「请求失败」绝大多数情况下不是权限本身没弹窗,而是「声明位置不对」或「跑的还是旧包」。
要同时满足两点才真正生效:
- 写在参与构建的那个
module.json5里,且位于module.requestPermissions数组。 权限是模块级声明,app.json5里写无效。多模块工程尤其要注意:是你真正发包/运行的 module(通常是entry)的module.json5,不是别的 module。 - 修改配置后必须 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。
| 错误码 | 官方语义 | 第一时间怀疑 | 该往哪查 |
|---|---|---|---|
| 201 | Permission denied | 没声明/没授予 INTERNET | module.json5 的 requestPermissions 与安装态(bm/atm dump) |
| 2300001 | Unsupported protocol | 协议不支持(如写了 ftp://) | 请求 URL 的 scheme |
| 2300003 | Invalid URL format or missing URL | URL 格式错误或缺 URL | request 的 url 参数 |
| 2300005 | Failed to resolve the proxy name | 代理名解析失败(配了代理) | 应用代理配置 |
| 2300006 | Failed to resolve the host name | DNS 解析失败 | 域名拼写、DNS 配置、内网 DNS、VPN/代理 |
| 2300007 | Failed to connect to the server | TCP 连不上 | IP/端口、防火墙、服务是否监听、网段是否可达 |
| 2300008 | Invalid server response | 服务端回复异常 | 服务端实现 |
| 2300028 | Operation timeout | 连接/读取超时 | 弱网、服务端响应慢、connectTimeout/readTimeout 设置 |
| 2300052 | The server returned nothing | 服务端空响应(无 header/data) | 服务端是否真的返回了内容 |
| 2300058 | Local SSL certificate error | 本地证书错误 | 客户端证书/CA 路径 |
| 2300060 | Invalid SSL peer certificate or SSH remote key | 对端证书无法认证 | 域名匹配、有效期、证书链、自签名 |
| 2300077 | SSL CA certificate does not exist or is inaccessible | CA 证书找不到/不可访问 | trust-anchors 配置与资源路径 |
| 2300997 | Cleartext traffic not permitted | 明文 HTTP 被拦 | network_config.json 的 cleartextTrafficPermitted |
| 2300998 | It is not allowed to access this domain | 不允许访问该域名(元服务/安全策略) | 域名白名单/安全策略 |
| 2300999 | Internal 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)
- 先打错误码:在
catch里打印BusinessError.code和完整message,这是分层的唯一可靠入口。 - 见到 201 /
ohos.permission.INTERNET check failed→ 查权限层:module.json5的requestPermissions是否写了、是否写在 module 而非 app、是否 Clean/Rebuild 并重装了新包;用bm dump/atm dump确认声明与授予。 - 见到 2300997 → 明文 HTTP 被拦:配
network_config.json的cleartextTrafficPermitted(仅调试),或改用 HTTPS。 - 见到 2300006 → DNS 层:换 IP 直连排除 DNS;查域名拼写、内网 DNS、VPN。
- 见到 2300007 → TCP 层:用
nc/telnet或本文 TCP 探测验证端口通不通;查防火墙、服务监听、网段。 - 见到 2300060 / 2300077 / 2300058 → TLS/证书层:查域名匹配、有效期、证书链、信任锚;先校正设备时间。
- 见到 2300028 / 2300052 → 超时/空响应层:查弱网、服务端响应、超时参数设置。
- 「无任何响应」 → 八成是异常被吞,或请求根本没执行/未完成就
destroy();在调用前、成功、失败三处都加日志。 - 真机调本地服务 → 用电脑局域网 IP(不要用
127.0.0.1/localhost),确认服务监听0.0.0.0、防火墙放行。 - 仍无头绪 →
hdc shell hilog过滤网络栈,带脱敏错误码、请求协议(HTTP/HTTPS)、SDK 版本、目标公网/局域网信息进一步定位。
最容易踩的坑
- 权限写在
app.json5:权限是模块级,必须写运行模块的module.json5的requestPermissions;app.json5里写了不生效。 - 改了配置没重装:只改工程不 Clean/Rebuild 并卸载重装,设备上还是旧包——「加了权限仍失败」头号原因。
- 真机写
127.0.0.1/localhost:那指向手机自己,不是开发电脑;要用电脑局域网真实 IP,服务监听0.0.0.0。 - 把服务端
4xx/5xx当成「请求失败」:401/403/404/500是服务端回了话的业务状态码(在response.responseCode里,走then分支),和 23 开头的请求异常码完全是两码事。混为一谈会让你在「服务端没问题」和「客户端没连上」之间反复横跳。 catch里只写{}或只打err.message:最有价值的BusinessError.code反而没打出来。请务必打印完整对象(JSON.stringify(err)),证书类问题常常直接写在message里。- socket 接口名:当前 TCP socket 构造接口是
socket.constructTCPSocketInstance(),旧版才是constructTCPSocket,照抄旧文档容易编译失败。 - 为排查而关闭证书校验:证书错误应修证书或正确配信任锚,不建议为「先跑通」而长期关闭校验,正式环境会留下安全隐患。
参考链接
- 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.socketSocket 连接(constructTCPSocketInstance / connect):https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V14/js-apis-socket-V14
更多推荐
所有评论(0)