如何统一返回 JSON 格式错误给前端?
解读
国内一线/二线公司面试时,这道题表面问“格式”,实则考察三层能力:
- 工程化意识:能否把“错误”从分散的
echo、die中收拢到一处,让前端拿到稳定结构。 - 安全与规范:是否知道隐藏敏感信息、遵守 HTTP 状态码语义、符合国内等保/密评要求。
- 性能与可维护:是否理解 PHP 生命周期,能在最合适的钩子点拦截错误,避免重复封装、重复序列化。
一句话:面试官想听“你怎么用 PHP 机制把错误变成前端可识别的 JSON,同时保证高内聚、低耦合、易扩展”。
知识点
- PHP 错误层级:Error、Exception、Throwable,以及
set_error_handler/set_exception_handler/register_shutdown_function三者区别。 - 自动加载与中间件:Composer PSR-4、PSR-15 中间件规范,Laravel/Symfony 异常渲染管道。
- 输出协议:国内主流
{code: number, msg: string, data: mixed, traceId?: string},以及 HTTP 状态码映射规则。 - 安全合规:生产环境屏蔽
file、line、trace,记录requestId方便链路追踪;符合《GB/T 35273 个人信息安全技术规范》。 - 性能细节:
header('Content-Type:application/json;charset=utf-8')必须在任何输出之前;使用JSON_THROW_ON_ERROR防止中文转义异常;OPcache 下避免在错误处理器里做重 IO。
答案
给出一个“公司可直接落地”的最小闭环方案,分三步:拦截 → 标准化 → 输出。
步骤 1:定义统一结构体
final class ApiResult
{
public static function fail(int $code, string $msg, $data = null): array
{
return [
'code' => $code,
'msg' => $msg,
'data' => $data,
'traceId' => $_SERVER['HTTP_X_TRACE_ID'] ?? uniqid('api-', true),
];
}
}
步骤 2:注册全局处理器(原生 PHP 项目放在 public/index.php 顶部;Laravel 放在 app/Exceptions/Handler.php;Symfony 放在 EventListener/ExceptionListener.php)
// 统一异常
set_exception_handler(function (Throwable $e) {
$code = $e->getCode() >= 400 && $e->getCode() < 600 ? $e->getCode() : 500;
$msg = env('APP_DEBUG') ? $e->getMessage() : '系统繁忙';
$data = env('APP_DEBUG') ? ['trace' => $e->getTraceAsString()] : null;
http_response_code($code);
header('Content-Type:application/json;charset=utf-8');
echo json_encode(ApiResult::fail($code, $msg, $data), JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
exit;
});
// 统一致命错误
register_shutdown_function(function () {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
// 记录日志,返回 500
call_user_func('exception_handler', new ErrorException(
$error['message'], 500, $error['type'], $error['file'], $error['line']
));
}
});
步骤 3:业务层只管抛异常,不用关心格式
if (!($user = User::find($id))) {
throw new BusinessException('用户不存在', 404);
}
结果:
前端收到的永远是
{"code":404,"msg":"用户不存在","data":null,"traceId":"api-664f9a8e9a3e27.53893432"}
且响应头 Content-Type: application/json; charset=utf-8,状态码 404。
拓展思考
- 多业务域错误码冲突:建议采用“领域+序号”方式,如
100001~199999用户域,200001~299999订单域,避免和 HTTP 状态码混淆。 - 国际化:在
ApiResult::fail里注入Lang::get($msg),根据Accept-Language自动返回中英文,出海项目必备。 - 链路追踪:把
traceId写入响应头X-Trace-Id,同时写入日志,接入阿里云 SLS、腾讯云 CLS,方便压测时快速定位。 - 错误分级告警:通过
Monolog\Handler\FingersCrossedHandler把 5xx 错误聚合后推送到企业微信/飞书,减少告警风暴。 - 性能极限优化:在 Swoole/FPM 协程场景下,异常处理器里避免使用
file_get_contents等阻塞 IO,可改用Swoole\Coroutine\System::writeFile异步写日志,防止错误雪崩。