Axios 全局 json-bigint 解析导致空原型对象渲染崩溃AI 主写
问题现象
在 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 时,内部大致会经过这样的路径:
-
将 value 写入 DOM: React 将
value赋给原生<input>元素的.value属性。DOM API 要求.value必须是字符串,因此浏览器会隐式调用String(value)进行转换。 -
String()的类型转换规则: 当String()遇到一个对象时,JavaScript 引擎按ToPrimitive尝试:- 先调用
obj[Symbol.toPrimitive]("string")(如果有的话) - 再调用
obj.toString()—— 期望返回一个 primitive - 再调用
obj.valueOf()—— 期望返回一个 primitive - 如果以上都失败或返回的不是 primitive,抛出
TypeError(见String)
- 先调用
-
空原型对象的致命问题: 通过
Object.create(null)创建的对象,原型链是空的。它没有继承Object.prototype上的toString()和valueOf()方法:普通对象: obj → Object.prototype → null ✅ toString() 可用 → "[object Object]" 空原型对象: obj → null ❌ 没有 toString(), 没有 valueOf() -
崩溃点: 浏览器调用
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 都只是后端未改前的临时手段,不应该长期使用。