冯老师的困惑 —— 诡异的 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-8UTF-16UTF-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 ,却让冯老师搭上了一顿猪头肉。生活不也是如此么,往往那些看似不起眼的细节,最后却让我们输得一塌涂地。

感谢大家的持续关注~

bom
本作品采用《CC 协议》,转载必须注明作者和本文链接
你应该了解真相,真相会让你自由。
本帖由 MArtian 于 2个月前 加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 16

大佬,问个简单的问题,为啥telescope不会去记录filament的post请求呢,php8.3,新装的laravel11框架,然后安装这两个包

2个月前 评论
小学毕业生 2个月前
lun1bz (作者) 2个月前
快乐的皮拉夫 (楼主) 2个月前
lun1bz (作者) 2个月前

可能有跨平台的需求吧?bom头,主要是方便跨平台系统或语言识别UTF编码格式用的,例如win notepad++文本编辑器或win 本地js工具,通过bom头来识别是否是UTF编码格式,不过现在UTF8已经是web开发通用编码规范了,所以没必要添加bom头,可能一些老的系统或功能考虑兼容需要,还会在文件或内容头部填充bom头。

2个月前 评论
快乐的皮拉夫 (楼主) 2个月前

这个问题以前被服务器还是window server 这里txt编辑器坑过。用postman返回数据正常,实际php解析是不对的。但是window11可以放心使用txt。

2个月前 评论
快乐的皮拉夫 (楼主) 2个月前

厉害厉害 :+1:

2个月前 评论

我之前还遇到过一次,旧win版本设置文件名,他的文件名前方有个 unicode 194 + 160,然后操作系统识别为空格,导致系统逻辑匹配不上,正则和php过滤函数也无法过滤,最后强行使用了 php preg_replace("/^[\s\v".chr(194).chr(160)."]+/","", $client_filename, -1, $is_fool) 然后才过滤得了

2个月前 评论
cxxxx

冯老师也是一位老演员了 :speak_no_evil:

2个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
文章
41
粉丝
121
喜欢
711
收藏
764
排名:249
访问:3.8 万
私信
所有博文
社区赞助商