`TypeError` 与 `Error` 混着抛,日志里只显示行号不显示堆栈:我整理的 PHP→JS 跨端异常"双语词典"

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
插件开发 53 浏览 0 回复

插件开发里最怕的不是报错,是报错信息"说了等于没说"。尤其是现在前后端混着写,PHP 抛个异常,AJAX 返回 500,浏览器控制台只收到 parsererror;或者 JS 里 await 了个 Promise,reject 回来的东西在 PHP 日志里完全没痕迹。两边各说各话,排查像猜谜。

这篇不聊具体业务,就整理我这一年攒下来的"双语对照"——同样一个根因,在 PHP 端和 JS 端分别长什么样,以及我最快定位到真凶的那条路径。


一、PHP 抛了,JS 只看到 200 或 500

最经典场景:admin-ajax.php 或者 REST 路由里,某个 WP_Query 参数写错了,PHP 直接 Fatal error。但前端 fetch 的 response.text() 拿到的是 HTML 错误页,JSON.parse 炸掉,JS 里变成 SyntaxError: Unexpected token <

这时候别盯着 JS 看。我的习惯是:先给 PHP 那端加个"临终捕获"。

// 在插件入口或者特定路由回调顶部
set_error_handler(function ($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) return false;
    throw new ErrorException($message, 0, $severity, $file, $line);
});

这样 E_WARNING 也能被转成异常接住,统一走你包的 try-catch,最终给前端返回标准 JSON:{'error': true, 'message': '...'}。前端再也不用猜 500 里头藏了什么。

反过来也成立:如果 JS 明明收到了 200,但 data.success 是 false,先检查是不是 PHP 里手动 wp_send_json_error() 时,把业务错误和系统错误混在一个通道里了。我现在的做法是分两层:success: false 只表示业务校验没过(比如积分不足),真正的异常走 HTTP 500,这样 Sentry 或者服务器日志能自动抓。


二、WP_Error 不是 Exception,throw 不出去

WordPress 很多核心函数返回 WP_Error 对象而不是抛异常。wp_insert_postwp_update_user 都是这德行。新手容易写成:

$post_id = wp_insert_post($data);
// 没检查 is_wp_error,直接往下用,结果 $post_id 是对象,进数据库查询时炸出 TypeError

PHP 8 之后更狠,传错类型直接 TypeError,但错误信息是 WP_Error given, int expected,行号指在很下游的位置,根本不在 wp_insert_post 调用点。

我的补丁:给常用函数包个"硬壳"。

function my_plugin_insert_post(array $data): int {
    $result = wp_insert_post($data, true); // 第二个参数 true,强制返回 WP_Error 而不是 0
    if (is_wp_error($result)) {
        throw new RuntimeException(
            'wp_insert_post failed: ' . $result->get_error_message(),
            0,
            new Exception($result->get_error_data() ? json_encode($result->get_error_data()) : 'null')
        );
    }
    return $result;
}

这样堆栈里能直接看到是谁传的脏数据,WP_Error 的原始信息塞在 previous 异常里也不丢。


三、JS 端的 undefined is not a function,根因在 PHP 输出的 HTML

插件后台页经常混用 PHP 生成初始数据和 JS 接管交互。如果 PHP 里某个 wp_localize_script 的变量因为权限问题没输出,JS 里 window.myPluginData.ajaxUrlundefined,然后 fetch(undefined) 不是报错,是发了个请求到当前页 URL,拿到 HTML,再走一遍 JSON.parse 炸掉。

这条链路的错误信息在浏览器里是 Uncaught (in promise) SyntaxError,行号指在 response.json(),完全误导。

我现在给所有 fetch 包一层"验尸官":

async function apiFetch(url, options = {}) {
    const response = await fetch(url, options);
    const contentType = response.headers.get('content-type') || '';
    
    if (!contentType.includes('application/json')) {
        const text = await response.text();
        console.error('Expected JSON, got:', text.slice(0, 200));
        throw new TypeError(`Server returned ${contentType}, status ${response.status}`);
    }
    
    return response.json();
}

这招帮我抓到过:安全插件拦截了 AJAX,返回 403 HTML 页;CDN 缓存了旧版本 JS,新 PHP 接口字段改名;甚至还有一次是服务器磁盘满了,PHP 输出了一半被截断,JSON 成了非法格式。


四、钩子里的"静默失败":异常被吞掉,页面白屏

WordPress 的 action/filter 回调如果抛异常,顶层通常有 try-catch 或者错误处理器,但位置很隐蔽。比如 do_action('wp_loaded') 里某个回调炸了,可能被 wp_die() 截住,也可能被主题的错误处理盖掉,表现就是页面加载到一半白屏,或者某个区块消失,error_log 里啥也没有。

我的笨办法:开发环境给所有核心钩子插"探针"。

add_action('all', function ($tag) {
    if (!in_array($tag, ['wp_loaded', 'init', 'admin_init', 'rest_api_init'])) return;
    
    $start = microtime(true);
    $current = current_filter();
    
    add_action($tag, function () use ($start, $tag) {
        error_log("[HOOK_PROFILER] {$tag} reached after " . round((microtime(true) - $start) * 1000, 2) . "ms");
    }, PHP_INT_MAX);
}, 1);

更狠一点,用 register_shutdown_function 抓未捕获异常:

register_shutdown_function(function () {
    $error = error_get_last();
    if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        error_log("[FATAL_SHUTDOWN] " . json_encode($error));
        // 如果还在 AJAX 上下文,强行输出 JSON 别让前端挂死
        if (wp_doing_ajax()) {
            wp_send_json_error(['fatal' => $error['message'], 'file' => basename($error['file'])]);
        }
    }
});

这段代码在生产环境慎用,但开发调试时救过我很多次——尤其是那种"本地正常,测试环境白屏"的玄学问题。


五、快速定位的"三板斧"

现在遇到跨端异常,我的顺序基本固定了:

1. 先看网络面板,不看 Console

确认 HTTP 状态码、Response 的 Content-Type、实际返回体前 200 字符。这能区分是 PHP 炸了、JS 解析错了、还是中间层(CDN/WAF)插了一脚。

2. 给两端加"握手标记"

每个 AJAX/REST 请求带个 X-Request-ID header,PHP 端生成 uniqid 写进日志。两边日志一对,秒级定位。

// PHP
$request_id = $_SERVER['HTTP_X_REQUEST_ID'] ?? uniqid('req_', true);
error_log("[{$request_id}] Entering handler");

// JS
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
fetch(url, { headers: { 'X-Request-ID': requestId } });

3. 怀疑堆栈说谎时,查 debug_backtrace

PHP 的 Exception::getTrace() 在闭

评论0
回复 · 0
还没有回复
微信客服 微信客服