Issue #76

Axios 全局 json-bigint 解析导致空原型对象渲染崩溃AI 主写

近 30 天0 次浏览0 位访客统计截至 2026-10-19 · GA4

问题现象

在 React 表单详情页中,页面加载时突然崩溃:

react-router-dom.js?v=cdf9f56e:6232 Error handled by React Router default ErrorBoundary: TypeError: Cannot convert object to primitive value
    at String (<anonymous>)
    at chunk-3GKRE62E.js?v=cdf9f56e:9785:69
    at Object.react_stack_bottom_frame (chunk-Z3WXWQW3.js?v=cdf9f56e:18509:20)
    at renderWithHooks (chunk-Z3WXWQW3.js?v=cdf9f56e:5654:24)
    at updateForwardRef (chunk-Z3WXWQW3.js?v=cdf9f56e:7198:21)
    at beginWork (chunk-Z3WXWQW3.js?v=cdf9f56e:8735:20)
    at runWithFiberInDEV (chunk-Z3WXWQW3.js?v=cdf9f56e:997:72)
    at performUnitOfWork (chunk-Z3WXWQW3.js?v=cdf9f56e:12561:98)
    at workLoopSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12424:43)
    at renderRootSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12408:13)
DefaultErrorComponent	@	react-router-dom.js?v=cdf9f56e:6232
react_stack_bottom_frame	@	chunk-Z3WXWQW3.js?v=cdf9f56e:18509
renderWithHooks	@	chunk-Z3WXWQW3.js?v=cdf9f56e:5654
updateFunctionComponent	@	chunk-Z3WXWQW3.js?v=cdf9f56e:7475
beginWork	@	chunk-Z3WXWQW3.js?v=cdf9f56e:8525
runWithFiberInDEV	@	chunk-Z3WXWQW3.js?v=cdf9f56e:997
performUnitOfWork	@	chunk-Z3WXWQW3.js?v=cdf9f56e:12561
workLoopSync	@	chunk-Z3WXWQW3.js?v=cdf9f56e:12424
renderRootSync	@	chunk-Z3WXWQW3.js?v=cdf9f56e:12408
performWorkOnRoot	@	chunk-Z3WXWQW3.js?v=cdf9f56e:11827
performWorkOnRootViaSchedulerTask	@	chunk-Z3WXWQW3.js?v=cdf9f56e:13505
performWorkUntilDeadline	@	chunk-Z3WXWQW3.js?v=cdf9f56e:36
react-router-dom.js?v=cdf9f56e:6232 Error handled by React Router default ErrorBoundary: TypeError: Cannot convert object to primitive value
    at String (<anonymous>)
    at chunk-3GKRE62E.js?v=cdf9f56e:9785:69
    at Object.react_stack_bottom_frame (chunk-Z3WXWQW3.js?v=cdf9f56e:18509:20)
    at renderWithHooks (chunk-Z3WXWQW3.js?v=cdf9f56e:5654:24)
    at updateForwardRef (chunk-Z3WXWQW3.js?v=cdf9f56e:7198:21)
    at beginWork (chunk-Z3WXWQW3.js?v=cdf9f56e:8735:20)
    at runWithFiberInDEV (chunk-Z3WXWQW3.js?v=cdf9f56e:997:72)
    at performUnitOfWork (chunk-Z3WXWQW3.js?v=cdf9f56e:12561:98)
    at workLoopSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12424:43)
    at renderRootSync (chunk-Z3WXWQW3.js?v=cdf9f56e:12408:13)
DetailForm.tsx:1273 TypeError: Cannot convert object to primitive value
    at String (<anonymous>)

The above error occurred in the <ForwardRef> component.

React will try to recreate this component tree from scratch using the error boundary you provided, AppErrorBoundary.
react-router-dom.js?v=cdf9f56e:6271 React Router caught the following error during render TypeError: Cannot convert object to primitive value
TypeError: Cannot convert object to primitive value
    at String (<anonymous>)

错误最终落在 Ant Design 的 <Input /> 渲染链路中。业务代码中存在一个隐藏表单项,用来保存联动配置对象:

<Form.Item name={[...formPrefix, configName, "nextConfig"]} hidden>
  <Input />
</Form.Item>

nextConfig 并不是字符串,而是一整个对象。此前页面可以正常运行,为什么突然开始报错?

背景: 为了解决大整数 ID 精度丢失

后端部分业务 ID 可能超过 JavaScript 的安全整数范围:

Number.MAX_SAFE_INTEGER = 9007199254740991

例如:

{
  "groupId": 9223372036854775807
}

如果使用浏览器原生 JSON.parse,这个 ID 会在解析时丢失精度。事后复盘,其实最合理的做法是让后端直接将这类 ID 序列化为字符串,前端就完全不需要特殊处理了。但当时后端短期内无法调整接口,只能前端兜底,于是我在 Axios 实例中全局启用了 json-bigint:

const JSONbigParser = JSONbig({ storeAsString: true });

export const AXIOS_INSTANCE = Axios.create({
  transformResponse: [
    (data: string) => {
      try {
        return JSONbigParser.parse(data);
      } catch {
        return data;
      }
    },
  ],
});

这个改动看起来很合理:

  • 超出精度范围的整数会保存为字符串
  • 普通数字仍然保持数字类型
  • 所有接口自动获得大整数兼容能力

但是正是这种全部接口的出参都替换导致了 bug

根因:json-bigint 返回空原型对象

这两种对象很像,但并不等价。

这不是推测,而是 json-bigint 在解析 JSON 对象的时候,sidorares/json-bigint 的 lib/parse.js 中, object() 在解析每个 { ... } 时显式写入:

object = function () {
  // Parse an object value.

  var key,
    object = Object.create(null);

  if (ch === '{') {
    next('{');
    // ...

所以经过 json-bigint 的对象,是通过 Object.create(null) 创建的。

然后普通对象继承了 Object.prototype:

const normal = {};

String(normal);
// "[object Object]"

空原型对象没有继承 toString、valueOf 等方法:

const parsed = Object.create(null);

String(parsed);
// TypeError: Cannot convert object to primitive value

因此,全局替换 Axios 响应解析器后,不只是超大整数发生了变化,所有接口返回的普通 JSON 对象也悄悄变成了空原型对象。

当 Ant Design 的 <Input /> 尝试将隐藏表单项中的 nextConfig 对象转换成字符串时,由于原型没有 toString() 就崩溃了。

<Form.Item name={[...formPrefix, configName, "nextConfig"]} hidden>
  <Input />  {/* value 会被注入为 nextConfig 对象本身 */}
</Form.Item>

然后由于 nextConfig 对应的 value 是一个对象,通过受控组件的写法注入了 input。此时 <Input /> 收到的 value 就是 nextConfig 这个对象 , 且是由 json-bigint 解析出来的空原型对象。

<Input /> 内部发生了什么?

Ant Design 的 Input 底层是对原生 <input> 元素的封装。当它处理 value 时,内部大致会经过这样的路径:

  1. 将 value 写入 DOM: React 将 value 赋给原生 <input> 元素的 .value 属性。DOM API 要求 .value 必须是字符串,因此浏览器会隐式调用 String(value) 进行转换。

  2. String() 的类型转换规则: 当 String() 遇到一个对象时,JavaScript 引擎按 ToPrimitive 尝试:

    • 先调用 obj[Symbol.toPrimitive]("string")(如果有的话)
    • 再调用 obj.toString() —— 期望返回一个 primitive
    • 再调用 obj.valueOf() —— 期望返回一个 primitive
    • 如果以上都失败或返回的不是 primitive,抛出 TypeError(见 String)
  3. 空原型对象的致命问题: 通过 Object.create(null) 创建的对象,原型链是空的。它没有继承 Object.prototype 上的 toString() 和 valueOf() 方法:

    普通对象:        obj → Object.prototype → null
                     ✅ toString() 可用 → "[object Object]"
    
    空原型对象:       obj → null
                     ❌ 没有 toString(), 没有 valueOf()
    
  4. 崩溃点: 浏览器调用 String(nextConfig) → 找不到 toString() → 找不到 valueOf() → 抛出 TypeError: Cannot convert object to primitive value。

为什么此前页面可以正常运行?

虽然把对象塞进 <Input /> 本身就不是规范的做法,但之前因为普通对象能隐式转换为字符串,所以 " 巧合地 " 没出问题。引入 json-bigint 后,所有接口返回的嵌套对象都变成了空原型对象,这个隐藏的隐患才被触发。

在引入 json-bigint 之前,Axios 使用浏览器原生的 JSON.parse 解析响应。原生 JSON.parse 返回的是普通对象(继承自 Object.prototype),因此:

// 原生 JSON.parse 返回的普通对象
String({ a: 1 });  // "[object Object]"  ✅ 正常

// json-bigint 返回的空原型对象
String(Object.create(null));  // TypeError ❌ 崩溃

修复思路

最好的方法:后端 ID 字段返回 string

推动后端将超出 JS 安全整数范围的 ID 直接序列化为字符串:

{
  "groupId": "9223372036854775807"
}

收益:

  • 前端不需要 json-bigint,也没有空原型对象的副作用
  • 字段语义明确,Web / 移动端 / 小程序一致,不依赖路由表或启发式
  • 后端通常只需调整序列化配置,改动成本低

这是唯一真正「字段层面」且没有精度竞态的方案。下文过渡方案仅在后端尚未配合时使用。

过渡方案: 按响应内容启用 json-bigint

需要去判断 原始 JSON 字符串里是否可能出现超大整数。

解析工具

import JSONbig from "json-bigint";

const JSONbigParser = JSONbig({ storeAsString: true });

/** 启发式: JSON 中出现 16 位及以上的整数,可能超出 Number.MAX_SAFE_INTEGER */
const MAY_HAVE_UNSAFE_INT = /:\s*-?\d{16,}(?:[,}\]\s]|$)/;

export function needsBigIntParse(raw = ""): boolean {
  return MAY_HAVE_UNSAFE_INT.test(raw);
}

/** json-bigint 解析,并将空原型对象恢复为普通对象 */
export function parseBigIntResponse(data: unknown): unknown {
  if (typeof data !== "string") return data;

  try {
    return JSONbigParser.parse(data, (_key: string, value: unknown) => {
      if (value && typeof value === "object" && !Array.isArray(value)) {
        return { ...value };
      }
      return value;
    });
  } catch {
    return data;
  }
}

export function parseJsonResponse(data: unknown): unknown {
  if (typeof data !== "string") return data;

  try {
    if (needsBigIntParse(data)) {
      return parseBigIntResponse(data);
    }
    return JSON.parse(data);
  } catch {
    return data;
  }
}

reviver 中的 { ...value } 必不可少: 只要走了 json-bigint,嵌套 object 就是 null 原型,必须还原,否则仍可能触发隐式 String() 崩溃。

总结

这次问题的核心是: 引入中间件或者做出会影响全局的变更的时候需要谨慎。

URL 白名单、字段层面特殊处理以及全局 json-bigint 都只是后端未改前的临时手段,不应该长期使用。

引用链接

  1. sidorares/json-bigint
  2. lib/parse.js
  3. ToPrimitive
  4. String

讨论

0 条留言

去 GitHub 留言 ↗

还没有留言,欢迎来打个招呼。

回到顶部