冯老师的困惑 —— 诡异的 JSON
JSON变异了
一大早,冯老师就在「奋战到天亮」群里碎碎念了。
冯老师:「好奇怪,请求客户的 API 接口,其他接口都正常,就这一条数据,json_decode 以后返回了一个 null 呢?」
群里鸦雀无声……
冯老师:「其他用户都没问题,就这一个客户有问题。」
Silence × 2……
冯老师:「而且前边后边都成功了,就这一个请求返回了一个 null,大佬们知道这是什么情况吗?」
Silence × 3……
冯老师:「关键日志里记录的请求返回值确实是一个 JSON ,怎么会解析成 null 呢?」
Silence × 4……
冯老师:「有人在吗?」
我终于看不下去了,于是率先打破了沉默——
我:「是不是好久没去吃水饺了,晚上去吃水饺吧!」
冯老师:「谁能帮我解决这个问题,赏猪头肉一顿。」
我:「成交。」
案发现场
根据冯老师的描述,已经初步得到以下结论:
- 目前仅发现一个用户的一个请求出现了这种情况。
- 异常请求的前后数据都是正常的。
- 接口返回的是 JSON 数据,通过
grep
命令查看,日志中记录的结果确实是一个标准的 JSON。 - 「标准」的 JSON 数据通过
json_decode
方法解析后,返回了 null 。
结果就是这么诡异,让人不明觉厉。
冯老师也的确没有说谎,毕竟日志都在那里摆着。那会是哪里出了问题呢?
意外破案
本着「冯老师说的话不可尽信」的原则,我仔细揣摩着冯老师说过的每一句话,力求一个标点符号也不放过。
「个别请求……前后正常……正常返回……异常解析……」
到底是哪一步出问题了呢?
json_decode
函数肯定不会有问题,毕竟是标准库函数。但解析的 JSON 看上去也没有什么异常啊,难道看到的和实际的还不一样?
因为冯老师是通过grep
命令查看的,我打算用less
命令,看看发生问题的 JSON 数据「到底」长个什么样。
不看不要紧,一看就发现了端倪——
<U+FEFF>{"status":"200","msg":"OK"}
与其他 JSON 数据不同的是,出问题的这条 JSON 前面多了个<U+FEFF>
的东西,看着这么眼熟呢?
网上一搜,果不其然,原来是 BOM 头:
U+FEFF 是一个 Unicode 字符,也被称为字节序标记(Byte Order Mark,简称 BOM )。它主要用于标识文本文件的字节序,即字节的高位在前还是低位在前。如果 U+FEFF 出现在文件的开头,它表示该文件使用 UTF-8、UTF-16 或 UTF-32 编码,并且标识了字节序。如果出现在文件的中间或结尾,则表示一个零宽度的非换行空。
看到这里,真相基本浮出水面了:客户返回 JSON 数据的时候,在该条 JSON 前追加了不可见的 BOM 头,导致json_decode
不能按正常的格式解析,返回了 null。
当我把截图发给冯老师的时候,冯老师的嘴轻轻一撇,露出一脸不屑:
「就这?我马上就查到这里了。」
「这次要大份的,小份不够吃。」
冯老师再次与成功失之交臂。
BOM 雷区
实际上,在日常工作中,BOM 是一个经常遇到的「雷区」,而且因为其本身具有「不可见性」的特点,不容易被排查出来。这里我们就来列举一下 BOM 常见的「雷区」,仅供大家参考:
1. 影响header
头设置
最常见的场景就是图片生成了。众所周知,PHP 通过设置 HTTP 响应头格式,将图片内容以二进制流数据返回给客户端,如果文件中包含了 BOM 头,图片就不能正常渲染了。
疑惑:不少文章说 BOM 头会影响 HTTP 头设置,例如重定向、状态码等,但是笔者在 PHP5.6 和 PHP 7.2 的环境中进行测试,没有发现这种问题。后续在其他环境上再进行验证一下,这里暂且不做讨论。
2. 影响数据解析
如果服务端在返回 JSON 格式的响应时,响应内容中带了 BOM 头,会影响客户端的正常解析。本文中出现的就是这种问题。
类似的情况还有 XML 格式的数据,都可能会对数据解析造成影响。
3. 影响正则判断
如果字符串中包含了不可见字符,可能还会影响到正则判断。这种在表单接收数据时经常会遇到,有时候明明看着数据是正确的,但却无法通过正则匹配。这可能是在字符串中包含了不可见的字符,需要重点排查下这种情况。
避坑指南
那一般如何避免 BOM 头带来的这些「坑」呢?一般我们会通过以下几种方式进行处理。
1. 编辑器设置
使用编辑器保存文件时,注意选择「无 BOM」的编码方式进行保存。
2. 使用工具或者函数去除 BOM 头
在 PHP 中可以使用以下方法检查并过滤掉 BOM 头:
// 移除 BOM 头函数
function removeBOM($file) {
$contents = file_get_contents($file);
$bom = pack('CCC', 0xEF, 0xBB, 0xBF);
if (substr($contents, 0, 3) === $bom) {
$contents = substr($contents, 3);
file_put_contents($file, $contents);
}
}
// 示例文件路径
$filename = 'demo.php';
// 调用函数移除 BOM 头
removeBOM($filename);
3. BOM 头过滤处理
如果字符串中包含 BOM 头或其他不可见字符,在 PHP 中可以使用 trim
函数进行过滤,这在处理第三方返回的数据时会比较有用。
总结
一个看似不起眼的 BOM ,却让冯老师搭上了一顿猪头肉。生活不也是如此么,往往那些看似不起眼的细节,最后却让我们输得一塌涂地。
感谢大家的持续关注~
本作品采用《CC 协议》,转载必须注明作者和本文链接
前排
大佬,问个简单的问题,为啥telescope不会去记录filament的post请求呢,php8.3,新装的laravel11框架,然后安装这两个包
沙发
:+1:
:+1:
可能有跨平台的需求吧?bom头,主要是方便跨平台系统或语言识别UTF编码格式用的,例如win notepad++文本编辑器或win 本地js工具,通过bom头来识别是否是UTF编码格式,不过现在UTF8已经是web开发通用编码规范了,所以没必要添加bom头,可能一些老的系统或功能考虑兼容需要,还会在文件或内容头部填充bom头。
这个问题以前被服务器还是window server 这里txt编辑器坑过。用postman返回数据正常,实际php解析是不对的。但是window11可以放心使用txt。
厉害厉害 :+1:
我之前还遇到过一次,旧win版本设置文件名,他的文件名前方有个 unicode 194 + 160,然后操作系统识别为空格,导致系统逻辑匹配不上,正则和php过滤函数也无法过滤,最后强行使用了
php preg_replace("/^[\s\v".chr(194).chr(160)."]+/","", $client_filename, -1, $is_fool)
然后才过滤得了冯老师也是一位老演员了 :speak_no_evil: