我准备连载一个原生鸿蒙开发系列,希望通过系列文章带你一同了解我是如何从0开发一款原生鸿蒙软件的,欢迎订阅关注。
一次持续 12 小时的原生鸿蒙开发调试经历

★ 我花了一周时间调试一个 HarmonyOS 网络请求问题。模拟器上 2010 条备忘录完整拉取,真机上同一份代码只拿到 175 条,而且全是 PUBLIC——没有一条 PRIVATE。我查了 HTTP 参数、协议版本、gzip 解压、缓冲区大小,甚至写了一篇报告断言问题是 gzip。但最终真相是:@kit.NetworkKit在真机上的 HTTP 请求特征,被服务端识别为"低权限客户端",数据被主动过滤了。
”
一、背景
我在做一个鸿蒙原生 memos 客户端。后端是 memos v0.29.1 + OpenResty,客户端用 @kit.NetworkKit(@ohos.net.http)发送 HTTP 请求。开发时一切顺利——Pura90 模拟器上 2010 条备忘录(含 PRIVATE)完美同步。
然后上真机。
同一份 HAP 包、同一网络、同一份 PAT 鉴权 Token,真机只拿到 175 条。全是 PUBLIC,PRIVATE 一条没有。
| | |
|---|
| 2010 条 | |
| 2010 条 | |
| HarmonyOS NEXT 真机 Pura90 | 仅 175 条 | |
模拟器正常、真机异常。这个问题本身就是一个反常识的信号。但我当时没在意,以为只是某个参数没对齐。
二、第一轮排查:那些"看似合理"的假设
2.1 HTTP 参数对齐
假设:鸿蒙 app 的 API 调用参数与 Android 版不一致。
| | |
|---|
filter | filter=creator=="users/gujin03" | |
pageToken | | |
| pageSize→pageToken→state→filter | |
全部对齐后,数据量仍 175。❌
2.2 HTTP 协议版本
假设:HTTP/2 在真机上有兼容问题。
真机 HTTP/2 报 2300999 Internal error,改成 HTTP1_1后稳定了,但数据量还是 175。模拟器依然 2010。
结论:问题不在协议版本,在真机 vs 模拟器的环境差异。
2.3 Gzip 解压假设(最有迷惑性的方向)
这是一个让我浪费了大量时间的假设。服务端响应体通过 Transfer-Encoding: chunked传输,我在模拟器上观察 response.result大小约 37KB,真机上只有约 24KB——差了约 40%。
| response len | |
|---|
| | 2010 条 ✅ |
| | 175 条 ❌ |
| | |
我写了一整份问题报告,断言是 @kit.NetworkKit没有自动解压 gzip,导致二进制压缩数据被当作 UTF-8 字符串解码后截断。
但我不放心,决定用 HttpShark(底层 libcurl)在真机上直接发同样的请求做个对比。
三、决定性证据:同一台真机上的对照实验
HttpShark 在同一台 Pura90 真机上、同网络、同 PAT,发送完全相同的 HTTP 请求:
| | | |
|---|
| | | |
| HttpShark/libcurl | Pura90 真机 | 30 条 | |
@kit.NetworkKit | | 25 条 | |
这个结果直接锁定了问题范围:
- ✅ 服务端正常——对真机 IP 返回的数据与电脑端一致
- ✅ 真机网络正常——libcurl 在真机上能完整收发
我当时写下了结论:"@kit.NetworkKit的 gzip 解压有 bug"。这是对的,但不完整。
四、第二次反转:不是 gzip
我准备提交 bug report 之前,决定在 VPS 上直接 curl 内部端口确认一下 gzip 的情况:
ounter(linecurl -sI http://127.0.0.1:5230/api/v1/memo?pageSize=30
结果:
ounter(lineounter(lineContent-Type: application/jsonContent-Length: 37541
没有 Content-Encoding: gzip。响应体以 {"memos":明文 JSON 开头。
我追查了两天的 gzip 假设,是错的。
五、根因发现:那 175 条数据全是 PUBLIC
真正的转折点来自一个被我反复忽略的细节——175 条数据全部是 PUBLIC。
我回想了服务端 memos 的行为:memos API 会根据客户端权限返回不同 visibility 的数据。如果是匿名或低权限请求,只返回 PUBLIC。
在 VPS 上用 curl 加同样 PAT 验证:
ounter(linecurl -s "https://memos.mysite.xyz/api/v1/memos?pageSize=200"
返回 200 条中,PRIVATE 有 191 条,PUBLIC 仅 9 条。PRIVATE 和 PUBLIC 散布在时间线上。
关键推理:如果只是数据截断(如 gzip 未解压),app 应该同时看到 PUBLIC 和 PRIVATE——因为它们在 JSON 中穿插排列。但 app 只看到 PUBLIC,说明服务端在返回数据之前就已经做了 visibility 过滤。
根因链至此浮现:
ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineapp 使用 @kit.NetworkKit 发送 HTTP 请求→ 真机上的 @kit.NetworkKit 实现与模拟器不同→ 发出的 TLS 指纹 / HTTP 请求特征独特→ OpenResty/memos 将其识别为"低权限客户端"→ 服务端主动过滤,只返回 PUBLIC 数据→ 175 条(全是 PUBLIC)
六、深度分析:@kit.NetworkKit的什么行为触发了过滤?
6.1 不是 gzip,是人
gzip 假设之所以有迷惑性,是因为数据量确实少了(24KB vs 37KB)。但这个数据差异是 visibility 过滤的结果——过滤后 JSON 体自然变小了,不是因为解压失败。
6.2 模拟器 vs 真机的差异根源
同一个 API 调用,模拟器上 @kit.NetworkKit请求的 TLS ClientHello 和 HTTP 头顺序,与真机不同。OpenResty 通过 lua-resty-limit-traffic或自定义检测规则,将真机的请求归类为"非标准客户端"。
6.3 为什么 HttpShark(libcurl)正常?
HttpShark 使用 libcurl 作为底层 HTTP 引擎,与鸿蒙 @kit.NetworkKit的网络栈实现完全独立:
- 不同的 TLS 客户端问候(ClientHello)
libcurl 的请求特征被 OpenResty 识别为标准桌面/服务端客户端,因此获得了完整权限。
@kit.NetworkKit在真机上的 HTTP 请求特征与标准浏览器/库不同,被服务端误判为低权限客户端——这就是原生鸿蒙的一个底层 bug。
七、解决方案与启示
7.1 终极方案:NAPI libcurl HTTP 模块
既然 @kit.NetworkKit的行为不可控,那就换掉它。通过 NAPI 将 libcurl 编译为原生 .so 模块,在 ArkTS 中调用:
ounter(lineounter(lineounter(lineounter(lineounter(lineArkTS HttpUtil.ts → curlNet.curlRequest({url, method, caBundle}) → C++ napi 桥接 → curl_easy_perform() 在 worker 线程执行 → 返回 Promise<T> 到 ArkTS
最终验证结果:
| | |
|---|
| | 5 次(pageSize=400) |
| | ~16 秒 |
| | 2004 条 |
| | |
7.2 经验与教训
模拟器正常不代表真机正常。@kit.NetworkKit在模拟器和真机上的底层实现不同,这个差异足以导致完全不同的服务端行为。
"数据少了"不等于 gzip。我花了两天追一个 gzip 假象,原因是我只看了"数据量少了"这个表象,却没有验证响应头的 Content-Encoding。
异常数据分布比数据总量更有价值。175 条全是 PUBLIC 这个信号被我忽略了整整两天。如果我在第一天就注意到这个模式,排查时间能缩短 80%。
抓包工具的对照实验是最有效的诊断手段。HttpShark(libcurl)在同一台真机上正常工作,直接锁定了问题范围。没有这个实验,我可能还在调 CURLOPT 参数。
错误信息要暴露原始内容。登录页 catch 块用固定文字掩盖 (e as Error).message,让 SSL 证书问题的排查多花了两天。
7.3 实践建议
| | |
|---|
| NAPI libcurl 替代 @kit.NetworkKit | |
| | |
| | |
| | |
最终,@kit.NetworkKit的 bug 并没有被修复——我换了一条路走。但这件事让我明白一个道理:在真机上看到的现象,如果在模拟器上复现不了,那问题大概率不在你的代码里,而在底层。原生鸿蒙还很年轻,有些 bug 是真实存在的。能做的就是发现它们、记录它们、然后找到自己的路绕过去。