Axios 上传大文件崩溃:鸿蒙 RNOH 下 XHR 返回空响应头引发的"假失败"

React Native + 鸿蒙适配项目中,上传小文件正常、上传大文件崩溃------而且后端其实已经收到了文件。一次诡异的偶发问题,最终靠 9 行 patch 解决。

问题

项目里有个文件上传功能,小图片上传一切正常,换成稍大的文件(~3MB)就崩了。错误信息很明确:

vbnet 复制代码
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:

js 复制代码
const responseHeaders = AxiosHeaders.from(
  'getAllResponseHeaders' in request && request.getAllResponseHeaders(),
);

正常情况下 getAllResponseHeaders() 返回类似 "content-type: application/json\n..." 的字符串,AxiosHeaders.from() 解析成功,一切正常。

但鸿蒙在大 multipart 响应下返回了 ''。追踪 AxiosHeaders.from('') 的调用链:

scss 复制代码
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 根本不会触发:

arduino 复制代码
正常流程: onloadend → 解析 headers → settle(resolve) → 拦截器 → 业务拿到 response
这次异常: onloadend → 解析 headers → throw → promise 未 settle → 拦截器不触发
                                                               → RN XHR onerror 兜底成错误

修复

patches/axios+1.6.7.patch

diff 复制代码
       // 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();
+      }

做了两件事:

  1. 空值守卫rawHeaders && String(rawHeaders).trim() 确保空字符串、null、纯空白都不进 parseHeaders,直接返回空的 AxiosHeaders
  2. try-catch 兜底 :万一遇到畸形字符串(比如 '\r\n\r\n')走到 parseHeaders 抛错,也不会阻断响应体解析

responseDatastatus 等主路径完全不动,补丁只管 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

经验

  1. 不要信任 RN / 鸿蒙的 XHR 实现完全符合 Web 规范 ------getAllResponseHeaders() 在规范里要求返回空字符串或合法 header 列表,但"空字符串"在 axios 的 parseHeaders 里恰好是个会抛错的值
  2. 偶发崩溃优先查平台差异------同一个 bug 只在特定平台出现,大概率是平台实现与预期不符
  3. patch 是务实的选择------精准改几行代码,最小化维护负担,等上游修复后直接删 patch 文件
相关推荐
倾颜1 分钟前
AI Mind 的单线程短期记忆设计:如何压缩、整理并控制当前会话上下文
前端·langchain·llm
u01437810823 分钟前
行内块元素之间的空白
前端·html
Lsx_24 分钟前
基于 Claude Code 的 AI 编程工作流探索
前端·ai编程·claude
yingyima38 分钟前
Nginx 配置速查手册:核心语法与实战案例
前端
梨子同志40 分钟前
开发工具
前端·后端
像我这样帅的人丶你还44 分钟前
Java 后端详解(六):Maven 与本地开发指南
前端·后端
触底反弹1 小时前
🎲 纯 CSS 搞定 3D 旋转立方体?还附赠一个「天坑」解决方案!
前端·css·html
盗德1 小时前
LangChain.js 中文开发手册(记录)
前端
Geek-Chow2 小时前
微服务认证与授权:01 — 概念
java·前端·数据库
狂炫冰美式2 小时前
凌晨睡不着,我给台风巴威写了个追踪网站
前端·后端·github