`rest_api_init` 钩子注册路由后,我的自定义端点仍返回 404:一场关于"钩子优先级"与"请求时机"的深夜追捕

插件开发 15 浏览 0 回复 返回上级

上周给会员系统加了个 REST 接口,本地 Postman 测得飞起,打包丢到测试环境直接 404。排查三小时后,发现是个老问题的新变种——不是路由没注册,是注册的时候请求已经跑完了。

先贴我当时"自信满满"的代码:

// 插件主文件顶部直接挂
add_action( 'rest_api_init', function () {
    register_rest_route( 'myplugin/v1', '/member/(?P<id>\d+)', [
        'methods'  => 'GET',
        'callback' => 'myplugin_get_member',
        'permission_callback' => '__return_true', // 调试用
    ] );
} );

看起来标准对吧?但问题藏在加载顺序里。我的插件用了懒加载策略,主文件里有个 MyPlugin::init() 静态方法,实际在 plugins_loaded 优先级 20 才执行。而 rest_api_init 的触发时机是:

// wp-includes/rest-api.php
do_action( 'rest_api_init' ); // 在 WP_REST_Server::serve_request() 里

关键点来了:rest_api_init 不是永久挂在那等你的。它只在 REST 请求被解析、WP_REST_Server 实例化之后触发一次。如果你的插件注册钩子的动作发生在这个时间点之后,那这次请求周期里路由表已经定型了,你再 register_rest_route 等于往闭卷考场上交答案。

验证方法很简单,在可疑位置加这段:

if ( did_action( 'rest_api_init' ) ) {
    error_log( '钩子已触发,当前优先级=' . current_priority() );
}

我在 MyPlugin::init() 里一测,did_action 返回 1,说明来晚了。路由注册代码执行时,WordPress 早就遍历完 rest_api_init 的所有回调,把路由表编译成正则缓存了。

第一个坑:钩子优先级数字游戏

有人可能想,那把优先级调到 1 或者负数?没用。rest_api_init 的触发是单次的,优先级只决定同一钩子下多个回调的执行顺序,不能让你穿越到钩子触发之前。核心问题在于——你的 add_action 本身必须在 rest_api_init 触发前执行。

第二个坑:条件判断的"善意谋杀"

更隐蔽的是这种写法:

add_action( 'plugins_loaded', function () {
    if ( ! wp_is_json_request() ) {
        return; // 非 REST 请求?那我不注册了,省资源
    }
    add_action( 'rest_api_init', ... );
}, 20 );

看起来精明,实则自杀。wp_is_json_request() 检测的是 Content-TypeAccept 头,但首次请求进来时,WordPress 可能还没解析到这一步,或者某些缓存/代理场景下头部信息不完整。更糟的是,如果你用 wp-json/ 前缀的漂亮链接,wp_is_json_requestparse_request 之前可能返回 false,导致你的路由注册被跳过。

我的实际场景更蠢:测试环境用了对象缓存,插件文件被 opcache 缓存后,我改了代码没重启 PHP-FPM,新逻辑根本没加载。Postman 一直 404,我却在代码里找了三小时,最后 opcache_reset() 一秒破案——这是额外赠送的踩坑,不提了。

根治方案:注册时机前置 + 防御性检测

现在我的插件入口长这样:

// 主文件,plugins_loaded 之前直接执行
add_action( 'rest_api_init', 'myplugin_register_routes', 5 ); // 优先级 5,留空间给依赖

// 或者更保守,不判断任何条件,无条件注册
function myplugin_register_routes() {
    // 这里可以做运行时条件判断,但注册动作本身必须发生
    if ( ! myplugin_module_enabled( 'member' ) ) {
        return;
    }
    
    register_rest_route( ... );
}

核心原则:路由注册要"早"且" unconditional "。条件判断放在回调内部或 register_rest_routepermission_callback 里,而不是决定是否注册路由。

调试工具箱

被 404 折磨时,这几招能救命:

  1. rest_get_server()->get_routes():直接 dump 完整路由表,确认你的路由在不在
  2. add_action( 'rest_api_init', function() { error_log( current_filter() . ' fired at ' . microtime() ); }, 1 ):确认钩子触发时机
  3. 检查 rewrite_rules 选项:某些伪静态冲突会让 wp-json 请求走不到 REST 解析器
  4. 直接访问 ?rest_route=/myplugin/v1/member/123 绕过漂亮链接,排除 rewrite 层干扰

最后问一嘴:你们有没有遇到过 rest_api_init 明明注册了、路由表里也看得见,但返回 403 且 permission_callback 根本没执行的情况?我下周可能要开新帖讲这个——rest_cookie_check_errors 与 nonce 校验的"提前截杀"机制。

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