先说安全:数据别离开你的电脑
出了问题,很多人的第一反应是搜一个「JSON 在线格式化」,把返回内容整段贴进去。可是接口返回里经常有用户姓名、手机号、邮箱、访问令牌和内部 ID,把 token 贴到第三方网站,就应该当作它已经泄露。本文用到的所有工具都完全在浏览器里运行,你可以在使用时打开开发者工具的「网络」面板自己确认:没有任何内容被发出去。所以排查时贴真实的返回、真实的 token 是可以的;不过养成习惯,排查完把复制来复制去的 token 作废,总归更稳妥。
问题一:「Unexpected token」,返回的不是合法 JSON
前端报错类似「Unexpected token } in JSON」。意思是服务器返回的东西看起来像 JSON,但违反了某条语法规则,解析器直接放弃了。把响应体贴进 JSON 格式化,点「格式化」。它不会只给一句笼统的报错,而是指出第一个错误在第几行第几列,以及最可能的原因。
比如这样一段返回:
{
"a": 1,
}格式化工具会报告第 3 行第 1 列,也就是那个右花括号,并说明 JSON 不允许尾随逗号。常见原因按出现频率大致是:
- 尾随逗号:多半来自模板拼接,或者手写的测试数据,每个字段后面都跟了逗号。
- 单引号:代码直接把 Python 的 dict 或 JavaScript 对象打印出来,而不是用序列化函数生成 JSON。
- 注释:配置文件里留着 // 或 /* */ 注释,却被当作 JSON 返回。
- 被截断:内容在某个字符串或对象中间戛然而止。这通常不是序列化的问题,而是超时、网关对响应大小的限制,或者写响应时进程崩溃。格式化工具会把它报告为「输入意外结束」。
- 返回的是 HTML 错误页:第一个字符是
<。去看 HTTP 状态码和 Content-Type 响应头,负载均衡返回的 502 页面不是业务代码的锅。
问题二:「记录不存在」,ID 被悄悄舍入了
解析一切正常,但拿 ID 去查详情时查不到,或者两条不同的记录看起来 ID 一样。把原始响应里的 ID 和代码实际用的 ID 对比一下:如果原始的是 12345678901234567890,日志里却是 12345678901234567000,那就是被舍入了。
JavaScript 的数字是 64 位浮点数,只能精确表示不超过 2^53 − 1,也就是 9007199254740991 的整数。很多数据库主键和 ID 生成器(比如雪花算法生成的 ID、64 位自增主键)都会超过这个范围。浏览器和 Node.js 里的 JSON.parse 遇到这种数字,会一声不响地换成最接近的可表示值。接口本身没错,是解析这一步把它改了。
本站的 JSON 格式化没有用 JSON.parse,数字按原样保留,所以格式化 {"n": 12345678901234567890} 得到的还是原来那串数字,可以用来快速看清真实的值。真正的修复在前后端:把 ID 以字符串形式返回(最稳妥,很多公开 API 都这么做),或者换用支持大整数的 JSON 解析库。后端用 Java 的,常见做法是给 Long 类型的 ID 字段配置序列化为字符串。
问题三:「401 Unauthorized」,看看 token 里写了什么
昨天还能调通的请求,今天统一返回 401。如果接口用的是 JWT 格式的 Bearer token,把 Authorization 请求头的值整段贴进 JWT 解码,开头的「Bearer 」会自动去掉。解码器会显示头部、载荷,最有用的是把时间类字段换算成日期。
以 RFC 7519 里的示例 token 为例,载荷中有 "exp": 1300819380,解码器会显示为 2011 年 3 月 22 日 18:43:00(UTC),并标记为已过期。实际排查中,最常见的发现是:
- exp 已经过去:token 过期了,刷新逻辑没有执行,或者执行失败却没有报错。
- nbf 在未来:token 还没生效,通常是签发服务器的时钟比校验服务器快。
- aud 或 iss 不对:测试环境签发的 token 被带到了生产环境。
- alg 是 none:有地方在签发不带签名的 token,服务端必须拒绝这种 token。
注意,解码器没法告诉你签名是否有效,它只显示 token 自己声称的内容。如果各项声明看起来都对,服务器还是拒绝,就去检查服务端配置的签名密钥和算法是否一致。
问题四:日期显示成 1970 年,或者几万年以后
日期字段显示成 1970 年 1 月 20 日,或者显示成几万年后的某一天,两种情况几乎都是秒和毫秒混用。1700000000 当作秒,是 2023 年 11 月 14 日 22:13:20(UTC);当作毫秒,就只是 1970 年 1 月 1 日之后大约 20 天。反过来,1700000000000 毫秒被当成秒,会落到公元 55840 年。
把数值贴进 时间戳转换。它按数字位数判断单位:不超过 11 位按秒,不超过 14 位按毫秒,不超过 17 位按微秒,更长按纳秒,并注明选了哪种,然后同时显示 UTC 时间和你所在时区的时间。一眼看出哪个读法对之后,修复办法就是在生成或读取它的代码里乘以或除以 1000,并在接口文档里写清单位。
另一个相关的坑是时区:Unix 时间戳永远是 UTC。如果日期正好差了你所在时区的偏移量,比如在北京差 8 小时,那时间戳本身没错,是显示时用错了时区。后端用 LocalDateTime 之类不带时区的类型往返转换时,尤其容易出现这种整 8 小时的偏差。
附:看起来像乱码的字段
有些接口把二进制数据或者嵌套的文档编码成 Base64 字符串返回:附件、签名、分页游标,或者 JSON 里再套一层 JSON。把值贴进 Base64 编解码,SGVsbG8sIOS4lueVjA== 会解出「Hello, 世界」;如果解出来不是文本,工具能识别 PNG、PDF 等常见格式,并提供下载。分页游标解码后常常是一个小 JSON 对象,能看出接口是怎么翻页的,排查「翻页时漏了一条或重复一条」这类问题很有用。
排查顺序小结
- 先看 HTTP 状态码和 Content-Type:不是 200 或者不是 application/json,问题多半不在数据本身。
- 解析报错,就用 JSON 格式化定位行列。
- 能解析但数据对不上,对比原始响应和代码里的值,重点看长数字 ID。
- 401、403,解码 token,看过期时间、生效时间和受众。
- 日期离谱,先猜单位,再查时区。
把问题报告给接口负责人
找到原因之后,一份好的问题描述能让对方一次改好,而不是来回沟通五轮。建议包含这几样:请求本身(方法、路径、相关的请求头,token 要删掉),HTTP 状态码,能说明问题的最小一段响应,以及一句话说明你期望的结果是什么。
比如「第 3 行第 1 列:最后一个字段后面多了逗号」,或者「id 12345678901234567890 是以数字返回的,前端解析后精度丢失,请改为字符串」,对方看了就能直接动手;而「接口坏了」「数据不对」只会换来一句「我这边是好的」。发出去之前,把样例里的手机号、身份证号、姓名这类个人信息删掉或者替换成假数据。用本站的 JSON 格式化把响应展开之后,很容易只截取出有问题的那一个字段。
如果问题是时间戳单位或时区,附上原始数值和你算出的两种读法,例如「1700000000 按秒是 2023-11-14 22:13:20 UTC,按毫秒是 1970-01-20」,对方马上就能判断是哪一边理解错了。约定清楚之后,最好顺手把单位写进接口文档,避免下一个对接的人再踩一次同样的坑。
局限
- 这些工具只检查数据,不重放请求。 要复现失败的请求,请用 HTTP 客户端或浏览器的网络面板。
- JSON 格式化只报告第一个语法错误。 改掉之后再格式化一次,才能找到下一个。
- 不校验 JWT 签名。 校验需要签发方的密钥,应该在服务端完成。
- 时间戳单位按位数判断。 1970 年代初期的值,或者单位本身不常见的值,可能判断错;转换工具总会注明它假定的单位。
常见问题
怎么找出 JSON 哪里写错了?
贴进 JSON 格式化,它会给出第一个错误的行号、列号和最可能的原因,比如尾随逗号或者用了单引号。
为什么长 ID 解析后变了?
JavaScript 的数字超过 2^53 就会丢失精度,JSON.parse 会悄悄把大整数舍入。把这类 ID 以字符串返回,或者使用支持大整数的解析库。
怎么看 JWT 有没有过期?
贴进 JWT 解码,它会把 exp 换算成日期,并提示是否已经过期。