React Native + 鸿蒙适配项目中,上传小文件正常、上传大文件崩溃——而且后端其实已经收到了文件。一次诡异的偶发问题,最终靠 9 行 patch 解决。
问题
项目里有个文件上传功能,小图片上传一切正常,换成稍大的文件(~3MB)就崩了。错误信息很明确:
Error: header name must be a non-empty string
at AxiosHeaders.set (axios/lib/core/AxiosHeaders.js:...)
at parseHeaders (axios/lib/helpers/parseHeaders.js:...)
at AxiosHeaders.from (axios/lib/core/AxiosHeaders.js:...)
at xhrAdapter.onloadend (axios/lib/adapters/xhr.js:97)
注意几个关键点:
- 栈打在
onloadend——响应已经回来了才报错 - 后台日志显示文件已经写入成功
- 同一套代码在 Android 上没问题,只有鸿蒙设备复现
- 登录等普通 JSON 接口完全正常,只在上传场景出现
一句话总结:HTTP 请求成功了,但客户端在解析响应头时崩了,业务拿到的是"假失败"。
复现
HarmonyOS 6.1.0 (API 23) 模拟器,同一份上传代码:
| 上传文件 | getAllResponseHeaders() 返回值 | 结果 |
|---|---|---|
| ~99 KB JPG | "content-type: application/json\r\n..." | 上传成功 |
| ~3 MB PNG | ""(空字符串) | 崩溃 |
小文件返回正常 header 字符串,大文件返回空字符串。跟图片格式无关,跟文件大小有关——更像是鸿蒙 XHR 在处理较大 multipart 响应时的时序问题。
根因
axios 的 xhr.js 在拿到响应后这样组装 headers:
const responseHeaders = AxiosHeaders.from(
'getAllResponseHeaders' in request && request.getAllResponseHeaders(),
);
正常情况下 getAllResponseHeaders() 返回类似 "content-type: application/json\n..." 的字符串,AxiosHeaders.from() 解析成功,一切正常。
但鸿蒙在大 multipart 响应下返回了 ''。追踪 AxiosHeaders.from('') 的调用链:
AxiosHeaders.from('')
→ new AxiosHeaders('')
→ this.set('')
→ 判断是字符串 → parseHeaders('')
→ ''.split('\n') → ['']
→ 循环处理: line = ''
→ i = ''.indexOf(':') → -1
→ key = ''.substring(0, -1).trim() → ''
→ headers.set('', value)
→ normalizeHeader('') → ''
→ throw 'header name must be a non-empty string'
空字符串按 \n 切割后得到一个空元素,解析出空 key,触发校验异常。
更要命的是这个异常的位置:它发生在 onloadend 内部,axios 的 promise 还没 settle。所以 拦截器也救不了——promise 链断了,interceptors.response 根本不会触发:
正常流程: onloadend → 解析 headers → settle(resolve) → 拦截器 → 业务拿到 response
这次异常: onloadend → 解析 headers → throw → promise 未 settle → 拦截器不触发
→ RN XHR onerror 兜底成错误
修复
patches/axios+1.6.7.patch:
// Prepare the response
- const responseHeaders = AxiosHeaders.from(
- 'getAllResponseHeaders' in request && request.getAllResponseHeaders()
- );
+ let responseHeaders;
+ try {
+ const rawHeaders =
+ 'getAllResponseHeaders' in request && request.getAllResponseHeaders();
+ responseHeaders =
+ rawHeaders && String(rawHeaders).trim()
+ ? AxiosHeaders.from(rawHeaders)
+ : new AxiosHeaders();
+ } catch (_err) {
+ responseHeaders = new AxiosHeaders();
+ }
做了两件事:
- 空值守卫:
rawHeaders && String(rawHeaders).trim()确保空字符串、null、纯空白都不进parseHeaders,直接返回空的AxiosHeaders - try-catch 兜底:万一遇到畸形字符串(比如
'\r\n\r\n')走到 parseHeaders 抛错,也不会阻断响应体解析
responseData、status 等主路径完全不动,补丁只管 headers。
为什么不用其他方案
| 方案 | 为什么不选 |
|---|---|
axios.interceptors.response 兜底 | promise 在 onloadend 里就断了,拦截器作为 promise 链上的节点不会触发 |
切换 adapter(http adapter) | 行为差异大,且项目依赖 XHR 特有的 onUploadProgress |
| 升级 axios | 查了 1.7.x 源码,同样没有防御;这是跨端环境的边界问题,axios 不太可能为此做适配 |
| 等 RNOH 修复 | 治本但周期不可控,业务不能等 |
业务侧配套
patch 解决了崩溃,但业务侧还有两件事需要处理:
- FormData 的 Content-Type:axios 默认会把 FormData 序列化成 JSON,需要在拦截器里对 FormData 请求设置
AxiosHeaders.setContentType(false),让浏览器/RN 自动处理 boundary - 更大文件的"假成功":patch 后不再崩了,但更大文件可能出现"流中断 + 后端返回 200 包裹业务错误码"的情况,业务侧需要主动判断
res.success,不能只看 HTTP status
经验
- 不要信任 RN / 鸿蒙的 XHR 实现完全符合 Web 规范——
getAllResponseHeaders()在规范里要求返回空字符串或合法 header 列表,但"空字符串"在 axios 的parseHeaders里恰好是个会抛错的值 - 偶发崩溃优先查平台差异——同一个 bug 只在特定平台出现,大概率是平台实现与预期不符
- patch 是务实的选择——精准改几行代码,最小化维护负担,等上游修复后直接删 patch 文件