路由注册"幽灵生效":我的 REST API 端点为何在 `rest_api_init` 里"注册成功"却永远 404

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

上周给插件加了个内部数据同步接口,代码写得行云流水——`register_rest_route` 包在 `rest_api_init` 里,namespace、route、method 一个不少,wp-json 一敲,404。更诡异的是,把同样代码扔进主题的 `functions.php`,活了。

先贴"看起来没问题"的死亡代码:

```php // 插件主文件 my-plugin.php add_action('rest_api_init', 'my_plugin_register_routes'); function my_plugin_register_routes() { register_rest_route('my-plugin/v1', '/sync/', array( 'methods' => 'POST', 'callback' => 'my_plugin_handle_sync', 'permission_callback' => '__return_true', // 调试用,别学 )); } ```

安装激活,访问 `/wp-json/my-plugin/v1/sync`,404。开 `WP_DEBUG_LOG`,`rest_api_init` 钩子列表里明明有我的回调,执行顺序也在 `rest_api_default_filters` 之后。但 `WP_REST_Server::get_routes()` 返回的数组里,就是没有 `my-plugin/v1`。

第一坑:插件加载顺序 vs REST API 初始化时机。WordPress 的 `rest_api_init` 在 `wp_loaded` 钩子中触发,而插件的加载是通过 `activate_plugin` 后的 `include_once`。问题出在——**我在插件激活时立即测试了接口**,此时 REST API 的 rewrite rule 还没被 flush。但这不是根本原因,因为手动去 设置→固定链接 点保存后,依然 404。

第二坑:namespace 的斜杠陷阱。`register_rest_route` 的第一个参数 `namespace` 官方说"不要前导或尾随斜杠"。我检查了,确实没有。但等等——我把路由定义拆到了 `includes/class-rest-api.php` 里,用 `require_once` 引入。这个文件在 `plugins_loaded` 时才加载,而 `rest_api_init` 的挂载是在主文件的 `add_action` 里。

真正的问题在这里:主文件的 `add_action('rest_api_init', ...)` 执行时,回调函数名 `my_plugin_register_routes` 是字符串引用。但那个函数实际定义在还没被 `require` 的文件里。PHP 不会报错,因为 `add_action` 只是注册回调引用,执行时才解析。然而——**我的 `require_once` 写在了条件分支里**,只在 `is_admin()` 为 true 时加载。

REST API 的前端请求,`is_admin()` 返回 false。所以回调函数从未被定义。`do_action('rest_api_init')` 执行时,调用不存在的函数,WordPress 的 `WP_Hook::apply_filters` 里会尝试 `call_user_func_array`,PHP 抛 `call_user_func_array() expects parameter 1 to be a valid callback, function 'my_plugin_register_routes' not found`——但错误被吞了,因为 REST API 请求的错误处理层级高,且我开了 `__return_true` 的权限回调掩盖了其他异常。

排查时我写的"钩子尸检"代码,贴在下面备用:

```php // 临时塞进 wp-config.php 或调试点 add_action('rest_api_init', function() { $hooks = $GLOBALS['wp_filter']['rest_api_init']->callbacks; error_log('=== rest_api_init 已挂载回调 ==='); foreach ($hooks as $priority => $callbacks) { foreach ($callbacks as $key => $callback) { $func = $callback['function']; $name = is_string($func) ? $func : (is_array($func) ? get_class($func[0]) . '::' . $func[1] : 'Closure#' . spl_object_hash($func)); $exists = is_callable($func) ? '✓ 可调用' : '✗ 不可调用'; error_log("优先级 $priority: $name [$exists]"); } } $server = rest_get_server(); error_log('=== 已注册路由 ==='); foreach ($server->get_routes() as $route => $handlers) { error_log($route); } }, PHP_INT_MAX); ```

这段输出让我看到:回调挂载了,但标记为 `✗ 不可调用`。顺着 `is_callable` 追下去,才发现函数定义文件根本没被加载。

第三个隐藏坑:修复后路由活了,但带参数的路由 `/sync/(?P\d+)` 又 404。这次不是加载问题,是正则转义。`register_rest_route` 的 `args` 里如果写了 `'validate_callback'`,但参数名 `id` 和路由里的 `(?P\d+)` 不一致——比如路由写 `(?P\d+)`,args 里配 `'id'`,匹配会成功但参数注入失败,返回 404 而非参数错误。这个坑更隐蔽,因为 WordPress REST 的报错不会告诉你"参数名对不上",而是直接走默认的 404 处理。

现在我的插件加载结构改成这样,彻底规避"条件加载导致回调悬空":

```php // my-plugin.php defined('ABSPATH') || exit; define('MY_PLUGIN_PATH', plugin_dir_path(__FILE__)); // 无条件加载核心文件,钩子注册与实现分离 require_once MY_PLUGIN_PATH . 'includes/class-loader.php'; $loader = new My_Plugin_Loader(); $loader->init(); // 内部统一挂载所有钩子 ```

`class-loader.php` 里再把 REST、Admin、Frontend 的初始化分门别类,但**文件加载本身不再依赖运行时判断**。钩子实现用 `add_action('rest_api_init', array($this->rest_api, 'register_routes'))`,对象方法引用,即使 `register_routes` 里抛异常,也能被准确捕获。

最后列个 checklist,给同样被 REST 路由 404 折磨的人:

1. `rest_api_init` 挂载的回调是否 `is_callable`?用上面的尸检代码扫一遍
2. 插件激活后是否 flush rewrite rules?`register_activation_hook` 里显式调用一次,或引导用户去保存固定链接
3. namespace 无斜杠,route 有前导斜杠(`'/sync/'` 而非 `'sync/'` 或 `'/sync'`)
4. 路由正则里的命名捕获组 `(?P...)` 与 `args` 配置里的键名完全一致
5. `permission_callback` 别用 `__return_true` 上线,但调试时如果 403,说明路由其实注册了,只是权限拦住,和 404 是不同问题

这次踩坑最深刻的教训:PHP 的字符串回调在 `add_action` 阶段是"延迟绑定",看起来注册成功,实际执行时函数可能不存在,而 WordPress 的钩子机制不会帮你验这个。对象方法回调至少会在 `add_action` 时就触发 `array($obj, 'method')` 的解析,早炸早发现。

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