本文约 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 );
常见踩坑
-
在
init上注册路由:rest_get_server()还没初始化,注册会被静默忽略,/wp-json/里看不到你的路由。必须rest_api_init。 -
rest_cookie_invalid_nonce的原因是缓存:页面缓存把带 nonce 的 HTML 存了下来,nonce 24 小时后过期,用户拿到旧 nonce,请求必失败。给带 nonce 的页面排除缓存,或改用 Application Passwords。 -
案例: 某站做了前后端分离,前端域名
app.example.com请求www.example.com/wp-json/,浏览器控制台一直报 CORS 错误,后端同事误判为「REST 路由有问题」改了一天路由。实际是 Nginx 没放行OPTIONS预检,且Access-Control-Allow-Credentials: true时Allow-Origin不能用*。加上正确的 CORS 头后一次通过。 -
安全插件拦截:Wordfence、iThemes Security 的「禁用 REST API」或「阻止未认证访问」选项会把正常请求变成 401,去插件设置里白名单
/wp-json/myplugin/*。 -
HTTPS 证书问题:
wp_remote_get请求自己站点的https://接口时证书校验失败,返回cURL error 60,看起来像接口挂了。 -
?rest_route=与/wp-json/结果不一致:说明有两层重写规则冲突(Nginx 与 Apache 并存、或多级反代)。 -
返回了 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 日志长期留着,下次几分钟就能定位。
