搜索全站
插件 19 分钟阅读

WordPress REST API 报错排查:权限回调与路由注册

/wp-json/ 返回 404、401、403?本文按 rest_no_route、rest_forbidden、rest_cookie_invalid_nonce 等真实错误码逐条拆解,给出路由注册的正确写法、权限回调加固定位代码与 curl 排查命令。

U
UFO_PF 2021-11-02 · 阅读 19 分钟
Wo 插件
阅读提示
本文约 7,381 字,预计阅读 19 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:接口昨天还好好的

前端同事说接口挂了,你打开 https://example.com/wp-json/wp/v2/posts,返回:

{"code":"rest_no_route","message":"未找到匹配 URL 和请求方式的路由。","data":{"status":404}}

或者更让人困惑的:浏览器里访问正常,用 Postman 请求却 401;登录了能访问,退出登录就 403;本地环境一切正常,上生产就 rest_cookie_invalid_nonce。

REST API 的错误都是结构化 JSON,每个错误码都指向一个明确的原因,比 PHP 白屏友好得多。关键是知道这几个码分别代表什么,逐个对号入座就能定位。

原理分析:错误码对照表

错误码 含义 常见原因
rest_no_route 路由不存在 插件未激活、固定链接为「朴素」、Nginx 未重写、路由注册在 init 之前
rest_forbidden / rest_cannot_access 权限回调返回 false permission_callback 逻辑太严、未登录、Capability 不足
rest_not_logged_in 需要登录 未带 nonce 或 Cookie,且权限要求登录
rest_cookie_invalid_nonce nonce 校验失败 nonce 过期(24 小时)、跨域请求未带 X-WP-Nonce、缓存了带 nonce 的页面
rest_missing_callback_param 缺少必填参数 请求未传必需参数
rest_invalid_param 参数不合法 args 里定义了校验规则,传入值不符合
rest_post_invalid_id 资源 ID 无效 文章被删除或不在该类型下

三个核心机制:

路由注册:register_rest_route( $namespace, $route, $args ) 必须在 rest_api_init 钩子上调用(不是 init)。namespace 是 /wp/v2 这种前缀,route 是 /posts/(?P<id>\d+) 这种带正则的路径。注册太早(init 之前)或太晚,路由都不存在。

权限回调:WordPress 5.5 起,permission_callback 是必填的。缺失时系统会给一个 __return_true(等于完全公开)并触发 _doing_it_wrong 通知。很多老插件没写这个参数,于是本该私有的接口被公开访问——这是安全问题不是报错,但安全插件会拦下来变成 403。

认证方式:浏览器里访问能成功,是因为带了登录 Cookie 和 X-WP-Nonce;Postman 直连没有这些,就被判为未登录。生产环境常用的认证是 Application Passwords(WP 5.6+ 内置)或 JWT 插件。

保姆级实操

第一步:确认路由是否真的注册上了

# 列出全部已注册路由与命名空间
curl -s https://example.com/wp-json/ | python -c "import sys,json;d=json.load(sys.stdin);print('\n'.join(sorted(d.get('routes',{}).keys())))"

# 直接查某个命名空间
curl -s https://example.com/wp-json/myplugin/v1 | head -c 600

# 用 WP-CLI 列出服务端的路由
wp rest api list 2>/dev/null || wp eval '
print_r( array_keys( rest_get_server()->get_routes() ) );'

wp rest api list 需要 WP-CLI 的 rest 命令(wp package install wp-cli/restful 或新版内置 wp rest)。如果命令行能列出、HTTP 请求却是 rest_no_route,问题一定在 Web 服务器层(重写规则),不在 PHP。

第二步:修服务器层的重写与拦截

# 确保 REST 路由能落到 index.php
location / {
    try_files $uri $uri/ /index.php?$args;
}

# 不要让 wp-json 命中任何静态/缓存规则
location ^~ /wp-json/ {
    try_files $uri /index.php?$args;
    add_header Cache-Control "no-store";
    fastcgi_cache_bypass 1;
    fastcgi_no_cache     1;
}

# 常见误伤:安全规则把 OPTIONS 预检请求挡了,跨域调用必挂
if ($request_method = OPTIONS) {
    add_header Access-Control-Allow-Origin "$http_origin";
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
    add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-WP-Nonce";
    add_header Access-Control-Allow-Credentials "true";
    return 204;
}

同时确认「设置 → 固定链接」不是「朴素」。朴素模式下 WordPress 仍能通过 ?rest_route=/wp/v2/posts 访问,但 /wp-json/ 这种美化路径依赖重写。

wp rewrite structure '/%postname%/'
wp option get permalink_structure

第三步:给缺失权限回调的路由补上(收紧安全)

老插件把接口默认公开了,不改插件文件也能在 rest_endpoints 阶段统一收紧:

<?php
// mu-plugins/rest-lockdown.php
add_filter( 'rest_endpoints', function ( $endpoints ) {
    foreach ( $endpoints as $route => $handlers ) {
        // 只处理我们关心的命名空间
        if ( ! str_starts_with( $route, '/myplugin/v1' ) ) {
            continue;
        }
        foreach ( $handlers as $index => $handler ) {
            // 老插件没写 permission_callback,WP 5.5+ 会填 __return_true
            if ( empty( $handler['permission_callback'] )
                 || '__return_true' === $handler['permission_callback'] ) {
                $endpoints[ $route ][ $index ]['permission_callback'] = function () {
                    return current_user_can( 'edit_posts' );
                };
            }
        }
    }
    return $endpoints;
} );

第四步:自己注册路由的正确写法

<?php
add_action( 'rest_api_init', function () {
    register_rest_route( 'myplugin/v1', '/export', [
        'methods'             => WP_REST_Server::READABLE,   // 等价于 GET
        'callback'            => 'mmf_export_callback',
        'permission_callback' => function ( WP_REST_Request $request ) {
            // 需要登录 + 具备导出能力
            return current_user_can( 'export' );
        },
        'args'                => [
            'post_type' => [
                'required'          => true,
                'validate_callback' => function ( $value ) {
                    return post_type_exists( $value );
                },
                'sanitize_callback' => 'sanitize_key',
            ],
            'limit' => [
                'default'  => 20,
                'sanitize_callback' => 'absint',
            ],
        ],
    ] );
} );

function mmf_export_callback( WP_REST_Request $request ) {
    $posts = get_posts( [
        'post_type'      => $request['post_type'],
        'posts_per_page' => $request['limit'],
    ] );
    return new WP_REST_Response( [ 'count' => count( $posts ) ], 200 );
}

要点:rest_api_init 挂载、permission_callback 必填、args 里做校验与净化(不做的话参数直接进数据库查询,是注入风险)、返回 WP_REST_Response 而不是 echo。

第五步:按认证方式分别排查

# 1) 未登录只读请求(公开数据,应 200)
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/wp-json/wp/v2/posts

# 2) Cookie + Nonce(浏览器同场景)
NONCE=$(wp eval 'echo wp_create_nonce("wp_rest");')
curl -s -H "X-WP-Nonce: $NONCE" -b "$(cat cookie.txt)" \
     -o /dev/null -w '%{http_code}\n' \
     https://example.com/wp-json/wp/v2/users/1

# 3) Application Passwords(服务端对服务端,需 HTTPS)
curl -s -u 'admin:xxxx xxxx xxxx xxxx xxxx xxxx' \
     https://example.com/wp-json/wp/v2/users/me

# 4) 看 OPTIONS 预检是否被放行
curl -s -X OPTIONS -i https://example.com/wp-json/wp/v2/posts \
     -H 'Origin: https://frontend.example.com' \
     -H 'Access-Control-Request-Method: POST' | head -20

第六步:打开 REST 请求日志

add_filter( 'rest_authentication_errors', function ( $result ) {
    if ( is_wp_error( $result ) ) {
        error_log( 'REST auth error: ' . $result->get_error_code() . ' - ' . $result->get_error_message() );
    }
    return $result;
} );

add_filter( 'rest_request_after_callbacks', function ( $response, $handler, $request ) {
    if ( is_wp_error( $response ) ) {
        error_log( sprintf(
            'REST %s %s -> %s',
            $request->get_method(),
            $request->get_route(),
            $response->get_error_code()
        ) );
    }
    return $response;
}, 10, 3 );

常见踩坑

  1. 在 init 上注册路由:rest_get_server() 还没初始化,注册会被静默忽略,/wp-json/ 里看不到你的路由。必须 rest_api_init。

  2. rest_cookie_invalid_nonce 的原因是缓存:页面缓存把带 nonce 的 HTML 存了下来,nonce 24 小时后过期,用户拿到旧 nonce,请求必失败。给带 nonce 的页面排除缓存,或改用 Application Passwords。

  3. 案例: 某站做了前后端分离,前端域名 app.example.com 请求 www.example.com/wp-json/,浏览器控制台一直报 CORS 错误,后端同事误判为「REST 路由有问题」改了一天路由。实际是 Nginx 没放行 OPTIONS 预检,且 Access-Control-Allow-Credentials: true 时 Allow-Origin 不能用 *。加上正确的 CORS 头后一次通过。

  4. 安全插件拦截:Wordfence、iThemes Security 的「禁用 REST API」或「阻止未认证访问」选项会把正常请求变成 401,去插件设置里白名单 /wp-json/myplugin/*。

  5. HTTPS 证书问题:wp_remote_get 请求自己站点的 https:// 接口时证书校验失败,返回 cURL error 60,看起来像接口挂了。

  6. ?rest_route= 与 /wp-json/ 结果不一致:说明有两层重写规则冲突(Nginx 与 Apache 并存、或多级反代)。

  7. 返回了 HTML 而不是 JSON:说明请求被重定向到了登录页或 404 页面(-L 跟随重定向看最终响应),不是 API 本身的问题。WordPress REST API 自定义接口

总结

判断标准:rest_no_route 查注册与重写;rest_forbidden 查 permission_callback;rest_cookie_invalid_nonce 查 nonce 与缓存;CORS 类报错查 Nginx 预检与响应头。

行动清单:先 curl /wp-json/ 列出路由,确认注册是否存在;不存在就查 rest_api_init 与固定链接/重写规则;存在但 403 就查 permission_callback 与安全插件白名单;nonce 类错误检查页面缓存是否缓存了带 nonce 的 HTML;跨域场景放行 OPTIONS 并正确设置 Access-Control-Allow-Origin(带凭证时不能用 *);服务端对服务端统一用 Application Passwords;最后加一段 rest_request_after_callbacks 日志长期留着,下次几分钟就能定位。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。