自定义路由 404 且 `template_redirect` 死活不触发:我翻遍 `rewrite_rules` 才发现 Nginx 和 WordPress 在"踢皮球"

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

上周接了个需求,要给插件做一套前端展示页,URL 格式是 /my-plugin/item/123。逻辑看着简单:注册 rewrite rule → 挂 template_redirect → 加载自定义模板。结果本地 Apache 环境跑得欢,往测试机 Nginx 上一丢,404 得整整齐齐,template_redirect 跟死了一样,var_dump 塞进去半点水花没有。

第一反应是规则没写进去。进后台刷新了固定链接(对,就是点那个"保存更改"),$wp_rewrite->rules 里明明有我的规则,优先级也没被顶掉。但前端就是 404。这时候我开始怀疑人生:WordPress 的 rewrite 和 Nginx 的 location 到底谁在撒谎?

第一坑:add_rewrite_rule 的坑位没占对

我最初把规则注册挂在 init,优先级 10,代码长这样:

add_action( 'init', function () {
    add_rewrite_rule(
        '^my-plugin/item/([0-9]+)/?$',
        'index.php?my_plugin_item_id=$matches[1]',
        'top'
    );
}, 10 );

看起来标准对吧?但问题在于 init 钩子执行时,$wp_rewrite 已经初始化完了,add_rewrite_rule 只是把规则塞进 $wp_rewrite->extra_rules_top,不会自动刷进数据库。本地 Apache 有 .htaccess 自动更新,Nginx 没有这待遇,规则只存在于内存,一刷新就没了。

正确姿势:要么在 register_activation_hook 里显式调用 flush_rewrite_rules(),要么给用户留个人工单点刷新。但更隐蔽的问题是——即使规则进了数据库,Nginx 配置不对,WordPress 根本收不到请求。

第二坑:Nginx 的 try_files 把皮球踢飞了

测试机的 Nginx 配置是从某面板一键生成的,location 块长这样:

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

表面看没问题,但我的 URL 是 /my-plugin/item/123,物理路径不存在,Nginx 按 try_files fallback 到 index.php,这时候 $query_string 是空的。WordPress 拿到请求后,靠 $_SERVER['REQUEST_URI'] 自己解析,但 Nginx 没把原始 URI 完整传过去——某些面板配置会多一层 break 或者 last,导致 WordPress 的 parse_request 阶段拿到的 URI 被截断或重写。

我当时的症状是:var_dump( $_SERVER['REQUEST_URI'] ) 输出的是 /index.php,而不是 /my-plugin/item/123。WordPress 拿着错误的 URI 去匹配 rewrite_rules,自然匹配不到我的规则,直接走 404 流程,template_redirect 根本没机会执行。

修复 Nginx 配置,把 try_files 改成显式传递 URI:

location / {
    try_files $uri $uri/ /index.php$is_args$args;
}

或者更保险地保留原始请求:

location / {
    try_files $uri $uri/ /index.php?$args;
}

改完 reload Nginx,REQUEST_URI 终于对了,但 template_redirect 还是不触发。继续挖。

第三坑:query_vars 没白名单,WordPress 直接过滤掉

就算 URI 对了,WordPress 解析出 my_plugin_item_id=123,但如果这个 query var 没注册,WP_Query 会把它当成非法参数丢弃。我的 template_redirect 判断条件是 get_query_var( 'my_plugin_item_id' ),结果永远返回空,钩子里的逻辑直接被跳过。

补上白名单:

add_filter( 'query_vars', function ( $vars ) {
    $vars[] = 'my_plugin_item_id';
    return $vars;
} );

这时候再测,template_redirect 终于活了。但等等,本地 Apache 之前没注册 query_vars 怎么也能跑?回头一查,Apache 的 .htaccess 重写规则在某些环境下会把所有参数原样丢给 index.php,WordPress 的 parse_request 有个兜底机制,会尝试从 $_GET 里捞未注册的变量——但这属于"侥幸",不能当规范。

第四坑:template_redirect 的优先级陷阱

就算前面都对了,如果你的 template_redirect 挂得太晚,也可能被别的逻辑截胡。比如某个 SEO 插件或者缓存插件,在同一个钩子上返回了 404 或者做了 exit。我排查时临时写了个"探照灯":

add_action( 'all', function ( $hook ) {
    if ( $hook === 'template_redirect' ) {
        error_log( 'template_redirect fired, priority check: ' . current_filter() );
    }
} );

更实用的做法是用 has_filter 扫描优先级:

global $wp_filter;
if ( isset( $wp_filter['template_redirect'] ) ) {
    foreach ( $wp_filter['template_redirect']->callbacks as $priority => $callbacks ) {
        error_log( "Priority $priority: " . count( $callbacks ) . ' callback(s)' );
    }
}

发现有个缓存插件在优先级 1 就 exit 了,我的逻辑挂在 10,永远执行不到。把优先级提到 -1 或者 0,问题解决。

复盘:我现在的"rewrite 自检清单"

这套组合拳打下来,我整理了个小清单,以后遇到路由不生效按顺序查:

  1. 规则有没有写进数据库? global $wp_rewrite; var_dump( $wp_rewrite->rules ); 看数组里有没有你的正则
  2. Nginx/Apache 有没有把请求正确交给 WordPress?$_SERVER['REQUEST_URI'] 是否完整
  3. query_var 有没有注册? get_query_var 返回空不一定是没匹配到,可能是被过滤了
  4. 钩子有没有被截胡? 扫描优先级,确认你的回调确实被执行
  5. 最后才怀疑 WordPress 核心 bug——十次有九次是自己漏了某一步

最讽刺的是,我在这三个坑里来回横跳了四个小时,最后修复的总代码量不到 10 行。但每个坑都涉及不同层级的协作:WordPress 的 rewrite API、Web 服务器的路由转发、PHP 的请求处理。插件开发越往后走,越觉得"代码写对"只是第一步,"请求能走到你的代码"才是更大的战场。

你们有遇到过类似的"多层甩锅"现场吗?比如 CDN 缓存把 template_redirect 的响应直接吞了,或者对象缓存导致 rewrite_rules 数组不同步?欢迎丢案例,一起完善这份 checklist。

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