
前言接口返回空白是 PHP 里最经典的一类问题。日志里接口状态码是 200前端JSON.parse报Unexpected end of JSON input抓包看响应体是零字节——排查半天最后发现代码里写的是echo json_encode($data);。真相是json_encode()失败时返回的是布尔值false而echo false输出的是空字符串。于是编码失败这个明确的错误被echo悄悄伪装成了返回空值。所以这个问题的本质不是函数返回了空值而是你丢掉了它返回false这件事以及后面那个真正的问题——为什么编码会失败。本文按先让错误现形再按错误码逐个击破的顺序展开先讲json_encode()三种看起来像空值的返回形态再讲最常见的非法 UTF-8 问题中文项目里十有八九是它然后给出一段可以直接跑的完整排查脚本。一、空值其实有三种先分清你看到的现象实际发生的事判断方法输出falseecho后页面空白编码失败函数返回falsejson_last_error()不为JSON_ERROR_NONE输出两个引号的你编码的就是一个空字符串输入本身是属正常结果输出null你编码的值本身是null输入是null属正常结果页面整体空白但它没到echo前面某处已报错/中断或输出缓冲被清掉看 PHP 错误日志、检查是否有exit/die网上大量json_encode 返回空的帖子最后定位到的其实是第四行的情况——编码本身没问题是流程提前中断了。所以第一步永远是把错误暴露出来不要用或者?? {}把它盖住。二、让错误现形json_last_error()与JSON_THROW_ON_ERROR?php declare(strict_types1); $data [name 张三\xB0\xA1]; // 故意混入非法 UTF-8 字节 $json json_encode($data); if ($json false) { // json_last_error_msg() 从 PHP 5.5 起可用直接给出人类可读的原因 fwrite(STDERR, 编码失败: . json_last_error_msg() . PHP_EOL); fwrite(STDERR, 错误码: . json_last_error() . PHP_EOL); } else { echo $json, PHP_EOL; }更省事的办法是JSON_THROW_ON_ERRORPHP 7.3 引入让失败直接抛异常从根上杜绝返回false被echo成空字符串?php declare(strict_types1); // PHP 7.3 try { $json json_encode($data, JSON_THROW_ON_ERROR); echo $json; } catch (JsonException $e) { // 异常里带了错误信息与错误码 error_log(json_encode 失败: . $e-getMessage() . code . $e-getCode()); http_response_code(500); echo {error:internal}; }标准错误码清单用常量名判断不要硬编码数字常量含义典型触发场景JSON_ERROR_DEPTH超出最大深度默认 512深层嵌套的树形结构JSON_ERROR_UTF8非法 UTF-8 字符GBK 数据、被截断的多字节字符JSON_ERROR_RECURSION存在循环引用对象互相持有引用JSON_ERROR_INF_OR_NAN出现了INF或NAN除零、log(0)等计算结果JSON_ERROR_UNSUPPORTED_TYPE不支持的类型编码了resource如文件句柄JSON_ERROR_INVALID_PROPERTY_NAME属性名不合法属性名含非法字节三、头号原因非法 UTF-8json_encode()要求所有字符串都是合法的 UTF-8。而现实中数据来源非常杂数据库连接没设charsetutf8mb4从 GBK 表里取出来的中文直接是 GBK 字节用户上传的文件名、Excel 导入的内容用了本地编码用substr()截断了中文字符串把某个汉字劈成半个这是最隐蔽的一种上一篇文章专门讲过第三方接口返回了编码不规范的数据。被截断的字符肉眼看不出来它依然是看起来正常的几个字直到编码失败。定位它的正确姿势是逐字节校验?php declare(strict_types1); // PHP 8.0 /** 递归找出第一个非法 UTF-8 的字符串返回它的路径 */ function findInvalidUtf8(mixed $value, string $path $): ?string { if (is_string($value)) { return mb_check_encoding($value, UTF-8) ? null : $path; } if (is_array($value)) { foreach ($value as $key $item) { $found findInvalidUtf8($item, $path . [ . var_export($key, true) . ]); if ($found ! null) { return $found; } } } return null; } $payload [ok true, list [[title 正常], [title 坏数据\xB0]]]; $badPath findInvalidUtf8($payload); echo $badPath null ? 全部合法\n : 第一个非法位置: {$badPath}\n; // 输出: 第一个非法位置: $[list][1][title]mb_check_encoding()属于mbstring扩展PHP 4.0.6 起就有不用担心版本。定位到具体字段后修复方式分两类方案 A从数据源头修正编码首选。如果是数据库来的先查连接字符集-- 确认表与列的字符集 SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA shop;并确保 PDO 连接串里带上charsetutf8mb4——这是最容易被忽略、又最容易解决的一条$pdo new PDO(mysql:host127.0.0.1;dbnameshop;charsetutf8mb4, $user, $pass);方案 B容忍脏数据用替代字符兜底。对于无法追溯来源的外部数据使用JSON_INVALID_UTF8_SUBSTITUTEPHP 7.2 引入它会把非法字节替换成 Unicode 替换字符UFFFD保证整体编码成功?php // PHP 7.2 $json json_encode($dirty, JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_UNICODE);替代字符在页面上显示为。这比整段失败要好但它只能算止血——真正该做的是把源头编码改对。四、其他几类失败原因的处置深度超限。默认深度 512足够应付正常业务如果确实需要更深的结构json_encode($data, 0, $depth)的第三个参数可以调大json_decode()的第三个参数也是深度这两处参数位置一致很好记。但更该反思的是结构本身超过 512 层的 JSON前端解析起来同样吃力。循环引用。典型的例子是两个对象互相持有对方?php // PHP 8.0 final class Node { public ?Node $parent null; public function __construct(public string $name ) {} } $a new Node(a); $b new Node(b); $a-parent $b; $b-parent $a; var_dump(json_encode($a)); // false var_dump(json_last_error_msg()); // Recursion detected处理方式是让模型不互相引用只保存 ID或在编码前用JsonSerializablePHP 5.4 起显式控制输出结构。INF/NAN。浮点运算的产物例如1/0在 PHP 8.0 起抛DivisionByZeroError或sqrt(-1)、log(0)。编码前用is_finite()校验?php // PHP 8.0 $stats [ratio log(0.0)]; // -INF foreach ($stats as $k $v) { if (is_float($v) !is_finite($v)) { $stats[$k] null; // 或者 0看业务语义 } } echo json_encode($stats, JSON_THROW_ON_ERROR), PHP_EOL; // {ratio:null}编码了资源。传进去的是fopen()的句柄、curl_init()的句柄或 PDO 连接这些都无法序列化。搜一下待编码数组里有没有这类对象即可。JSON_PARTIAL_OUTPUT_ON_ERROR是把双刃剑。它能让编码在出错时仍输出部分结果PHP 5.5 引入避免整体失败但代价是错误被静默吞掉数据可能悄悄地少一段。只在明确知道并接受这个后果时使用。五、代码实战一个能定位问题的编码封装把前面的判断揉成一个可以复用的函数出错时给出可直接定位的信息而不是返回一个让人误会的空串。?php declare(strict_types1); // PHP 8.0用到了 JsonException、match、nullsafe 等 8.0 特性 /** * 安全编码出错时抛出带上下文的异常绝不返回 false * throws RuntimeException */ function jsonEncodeStrict(mixed $value, int $flags 0, int $depth 512): string { try { return json_encode( $value, $flags | JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE, $depth ); } catch (JsonException $e) { // 把 PHP 的错误码翻译成可操作的建议 $hint match ($e-getCode()) { JSON_ERROR_UTF8 存在非法 UTF-8 字节检查数据源编码或用 JSON_INVALID_UTF8_SUBSTITUTE 兜底, JSON_ERROR_DEPTH 嵌套层级超过 . $depth . 请调整数据结构或提高 depth, JSON_ERROR_RECURSION 存在循环引用请检查对象互相持有的属性, JSON_ERROR_INF_OR_NAN 存在 INF/NAN 浮点值编码前请用 is_finite() 校验, JSON_ERROR_UNSUPPORTED_TYPE 包含不可序列化的类型如 resource, default 未知错误 . $e-getMessage(), }; throw new RuntimeException(json_encode 失败: . $hint, $e-getCode(), $e); } } // —— 验证三种典型场景 —— $cases [ 正常数据 [id 1, name 张三], 非法 UTF-8 [name 张三\xB0\xA1], 含 NAN [ratio NAN], ]; foreach ($cases as $label $value) { try { echo $label, , jsonEncodeStrict($value), PHP_EOL; } catch (RuntimeException $e) { echo $label, 失败: , $e-getMessage(), PHP_EOL; } }运行需要 PHP 8.0php json_demo.php输出正常数据 {id:1,name:张三} 非法 UTF-8 失败: json_encode 失败: 存在非法 UTF-8 字节检查数据源编码或用 JSON_INVALID_UTF8_SUBSTITUTE 兜底 含 NAN 失败: json_encode 失败: 存在 INF/NAN 浮点值编码前请用 is_finite() 校验在 Web 场景中把echo json_encode($data);全部换成echo jsonEncodeStrict($data);并在全局异常处理里输出 500就能把接口静默返回空白变成日志里一行明确的失败原因。常见坑点1. 用echo输出false的返回值❌echo json_encode($data);失败时输出空字符串看起来像返回空值。 ✅echo json_encode($data, JSON_THROW_ON_ERROR);或先判断$json false。2. 数据库连接没设字符集❌ DSN 只写mysql:host...;dbname...取出的中文是乱码字节编码时报JSON_ERROR_UTF8。 ✅ DSN 补上charsetutf8mb4并确认表字符集也是utf8mb4。3. 用substr()截断中文后再编码❌mb_substr写成substr($title, 0, 50)把某个汉字劈成半个整段 JSON 编码失败。 ✅ 截断一律用mb_substr($s, 0, 50, UTF-8)字数统计用mb_strlen()。4. 把JSON_INVALID_UTF8_SUBSTITUTE当成终极方案❌ 全站加上这个 flag脏数据变成也无所谓。 ✅ 它只用于无法追溯来源的外部数据自家数据必须回到源头把编码修正。5. 忘记处理浮点异常值❌ 统计接口算了平均值某个分母为 0 产生了NAN整份报告返回空白。 ✅ 计算后统一is_finite()校验非有限值转成null并记录告警。6. 编码结果前面混入了 BOM 或空白❌ 某个require的文件存成了带 BOM 的 UTF-8响应体开头多了三个字节前端解析直接失败。 ✅ 检查入口文件的编码保存为UTF-8 无 BOM并在发送 JSON 前调用header(Content-Type: application/json)明确类型。7. 空数组被期望成空对象❌ 前端期望{}PHP 里json_encode([])输出的是[]前端取属性报错。 ✅ 需要空对象时用json_encode(new stdClass())或使用JSON_FORCE_OBJECT注意它会把所有数组都变成对象。8. 生产环境关掉错误显示导致什么都看不到❌display_errorsOff且没配error_log编码失败的提示彻底消失。 ✅ 保留error_log记录并在异常处理里显式输出 500 JSON 错误体避免又返回一个空响应。总结现象真实原因解决方向响应体 0 字节编码返回falseecho成空串加JSON_THROW_ON_ERROR让异常暴露提示Malformed UTF-8数据含非法 UTF-8 字节修数据源编码必要时用JSON_INVALID_UTF8_SUBSTITUTE深层结构失败超出 depth默认 512调大第三个参数或重构数据结构对象编码失败循环引用用JsonSerializable控制输出或改为只存 ID统计接口偶发空白出现INF/NAN编码前用is_finite()归一化前端报解析错误静默失败或混入了额外输出统一响应封装设置正确的Content-Type回到标题里的问题json_encode()从来不返回空值它返回的是false。把false变成异常问题就从接口返回空白变成了Malformed UTF-8 characters出现在$data[list][1][title]——后者是一分钟能修完的事。这是处理这类问题唯一值得投资的动作也应当成为全站 JSON 输出的默认写法。