最近我发现 WordPress 站点上的一个异常。
我的中文主站和 English 子站都使用 Post Views Counter 统计文章浏览量。中文站看起来一直正常,但 English 子站最近发布的几篇文章,后台浏览量却长时间停留在 0。
刚开始我还以为只是访问量比较少,或者 WordPress 后台数字没有及时刷新。
但连续观察几篇文章以后,感觉不太对:我自己明明打开过这些页面,浏览量却仍然没有任何变化。
于是开始进一步排查。
一、最开始的现象:English 最近文章浏览量一直是 0
后台文章列表中,可以看到最近几篇 English 文章的浏览量没有增长。

最开始考虑过几个可能:
- 文章实际上没有产生有效访问;
- Post Views Counter 本身没有正常工作;
- Cloudflare 或 W3 Total Cache 影响了计数;
- REST API 被安全规则拦截;
- WordPress 后台显示值和数据库实际值不同步。
为了避免继续猜测,最直接的办法还是打开浏览器开发者工具,查看一次真实的计数请求。
二、真正的异常:Post Views Counter REST API 返回 403
Post Views Counter 当前使用的是 REST API 计数模式。
访问文章时,浏览器会发送类似下面的 POST 请求:
/wp-json/post-views-counter/view-post/{post_id}正常情况下,这个请求应该返回:
HTTP 200但实际看到的却是:
HTTP 403
继续查看响应内容,WordPress 返回:
{
"code": "rest_cookie_invalid_nonce",
"message": "Cookie 检查失败",
"data": {
"status": 403
}
}
后来在 www 主域上也能够看到同类 403 REST 响应。

这一步已经基本排除了“单纯是 Cloudflare WAF 拦截”的可能。
请求实际上已经到达 WordPress,真正返回 403 的是 WordPress REST API 自己。
问题开始集中到一个地方:
浏览器发送的 REST nonce 已经失效。
三、请求内容正常,真正异常的是 X-WP-Nonce
先查看 Post Views Counter 实际发送的请求内容。
请求中包含:
{
"storage_type": "cookies",
"storage_data": ""
}
也就是说,PVC 正常按照 Cookies 存储模式发送计数请求。
继续查看请求头,可以看到:
X-WP-Nonce: add9d7782a
随后直接在服务器上验证这个 nonce:
wp eval --allow-root '
$nonce = "add9d7782a";
echo "cached_nonce={$nonce}\n";
$result = wp_verify_nonce( $nonce, "wp_rest" );
echo "verify_result=";
var_dump( $result );
echo "current_nonce=" . wp_create_nonce( "wp_rest" ) . "\n";
'当时得到:
cached_nonce=add9d7782a
verify_result=bool(false)
current_nonce=68aeb1c609也就是说:
add9d7782a这个 nonce 确实已经失效。
接下来的问题就变成了:
为什么前台页面还在继续使用这个已经失效的 nonce?
四、检查 Post Views Counter 当前配置
为了排除插件设置本身的问题,我又重新检查了 Post Views Counter。
当前主要配置为:
- Counting Mode:REST API;
- Data Storage:Cookies;
- Count Interval:1 小时;
- 统计文章、页面等正常启用。

继续检查计数和访客存储相关选项,也没有发现明显错误。

缓存相关设置中,还可以看到插件自身的缓存兼容功能并没有实际参与当前计数。

与此同时,我的网站使用 W3 Total Cache,页面缓存生命周期大约为 4 天。
到这里,一个很重要的时间差开始出现:
页面缓存:约 4 天
WordPress REST nonce:
实际有效时间通常只有约 12~24 小时两者显然不是同一个生命周期。
五、根因:缓存页面中保存了一个生命周期更短的 nonce
继续检查 Post Views Counter 源码,可以看到 REST API 模式会生成:
wp_create_nonce( 'wp_rest' )然后把这个 nonce 随页面 JavaScript 参数输出给浏览器。
问题就在这里。
页面第一次生成时:
生成 HTML
↓
生成一个 wp_rest nonce
↓
nonce 被写入 HTML / JavaScript
↓
整个页面进入 W3 Total Cache随后过了一段时间:
wp_rest nonce 已经失效但是:
W3TC 页面缓存仍然有效于是访客继续拿到原来的缓存页面。
页面中的 JavaScript 仍然携带旧 nonce:
旧缓存页面
↓
旧 X-WP-Nonce
↓
POST /wp-json/post-views-counter/view-post/...
↓
WordPress REST Authentication
↓
403 rest_cookie_invalid_nonce最终就表现为:
页面访问正常,但 Post Views Counter 浏览量不再增长。
WordPress nonce 也不是“从生成时间开始固定 24 小时有效”。
它使用时间片机制,因此实际可用时间通常处于大约 12~24 小时之间。
所以即使一篇文章看起来还没有缓存满一天,也完全可能已经携带一个无法通过验证的旧 nonce。
六、不发送旧 nonce 时,REST 计数接口本身正常
为了确认是不是 Post Views Counter REST Endpoint 本身损坏,我又直接进行测试:
curl -i -sS \
-X POST \
'https://en.shuijingwanwq.com/wp-json/post-views-counter/view-post/27585' \
-H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
--data 'storage_type=cookies&storage_data='这次不发送页面中的旧:
X-WP-Nonce结果接口直接返回:
HTTP 200并且:
{
"post_id": 27585,
"counted": true,
"type": "post"
}这说明:
Post Views Counter 的公开计数接口本身没有坏。
问题就是缓存页面所携带的失效 X-WP-Nonce。
七、没有切换计数模式,而是增加一个很窄的兼容层
Post Views Counter 还提供 PHP、JavaScript 等计数模式。
但综合考虑以后,我没有切换。
因为我的网站使用完整页面缓存,REST API 本来就是更适合这种架构的计数方式之一。
如果为了这个问题切换计数模式,反而可能引入新的缓存兼容问题。
最终采用的方案是:
保留 REST API,保留页面缓存,只在 Post Views Counter 的
view-post请求携带失效 nonce 时,在 WordPress Core 正式进行 REST Cookie Authentication 以前替换成一个当前有效的 nonce。
没有修改 Post Views Counter 插件源码,而是增加了一个 MU-plugin:
wp-content/mu-plugins/post-views-counter-rest-cache-compat.php代码如下:
<?php
/**
* Plugin Name: Post Views Counter REST Cache Compatibility
* Description: Refresh stale cached REST nonces for Post Views Counter public view requests.
*/
defined( 'ABSPATH' ) || exit;
function shuijingwan_pvc_refresh_stale_rest_nonce( $result ) {
if ( null !== $result ) {
return $result;
}
if (
! isset( $_SERVER['REQUEST_METHOD'] )
|| strtoupper( (string) $_SERVER['REQUEST_METHOD'] ) !== 'POST'
) {
return $result;
}
if ( ! function_exists( 'post_views_counter' ) ) {
return $result;
}
$pvc = post_views_counter();
if (
! is_object( $pvc )
|| ! isset( $pvc->options['general']['counter_mode'] )
|| $pvc->options['general']['counter_mode'] !== 'rest_api'
) {
return $result;
}
$route = '';
if (
isset( $GLOBALS['wp'] )
&& isset( $GLOBALS['wp']->query_vars['rest_route'] )
) {
$route = (string) $GLOBALS['wp']->query_vars['rest_route'];
}
if ( $route === '' ) {
$request_uri = isset( $_SERVER['REQUEST_URI'] )
? (string) $_SERVER['REQUEST_URI']
: '';
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
if ( is_string( $path ) ) {
$prefix = '/' . trim( rest_get_url_prefix(), '/' ) . '/';
$position = strpos( $path, $prefix );
if ( false !== $position ) {
$route = '/' . ltrim(
substr( $path, $position + strlen( $prefix ) ),
'/'
);
}
}
}
if (
! preg_match(
'#^/?post-views-counter/view-post(?:/\d+)?/?$#',
$route
)
) {
return $result;
}
if (
! isset( $_SERVER['HTTP_X_WP_NONCE'] )
|| $_SERVER['HTTP_X_WP_NONCE'] === ''
) {
return $result;
}
$nonce = (string) $_SERVER['HTTP_X_WP_NONCE'];
if ( wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return $result;
}
$_SERVER['HTTP_X_WP_NONCE'] = wp_create_nonce( 'wp_rest' );
return $result;
}
add_filter(
'rest_authentication_errors',
'shuijingwan_pvc_refresh_stale_rest_nonce',
99
);这里使用:
rest_authentication_errors的 99 优先级,是因为 WordPress Core 的:
rest_cookie_check_errors默认在 100 优先级执行。
因此兼容代码可以在 Core 正式验证 REST nonce 以前完成处理。
它的作用范围也非常有限:
POST
+
Post Views Counter
+
REST API 计数模式
+
view-post Endpoint
+
请求存在 X-WP-Nonce
+
nonce 已经失效如果 nonce 本身有效,它不会做任何修改。
如果未来不用 REST API 模式,它也会自动停止介入。
八、修复后,English 浏览量开始发生变化
部署兼容代码以后,很快就可以在后台看到 English 文章的浏览量开始发生变化。

不过这还不足以证明一定是 MU-plugin 生效。
因为还有一种可能:
页面缓存刚好重新生成了,于是页面自然获得了新的 nonce。
所以还需要更严格的验证。
九、关键验证:故意不清页面缓存
部署 MU-plugin 以后,我没有立即清理 W3 Total Cache 页面缓存。
这是故意的。
因为如果把缓存清掉,再访问页面就会生成新的 nonce,那就无法判断究竟是兼容层修复成功,还是单纯因为缓存刷新。
继续访问原来的缓存页面后,REST 请求已经变成:
HTTP 200
更重要的是,检查请求头以后发现:
浏览器发送的仍然是原来那个旧 nonce。

也就是说:
页面缓存没有换
旧 nonce 也没有换但服务端已经能够正常接受请求。
随后响应内容显示:
{
"post_id": 27585,
"counted": true,
"type": "post"
}
这一步基本构成了完整闭环:
旧缓存页面仍存在
+
浏览器仍发送失效 nonce
+
MU-plugin 将其替换为当前有效 nonce
+
WordPress REST Authentication 通过
+
PVC counted:true因此可以确认:
不是页面缓存碰巧刷新了,而是服务端兼容代码真正解决了问题。
十、期间出现过一次 Cloudflare 520,但属于另外一个问题
测试过程中还碰到过一次:
Cloudflare Error 520
一开始也担心会不会和刚部署的 MU-plugin 有关系。
不过立即重新测试同一篇文章以后,请求又恢复了正常:
HTTP 200
counted:true而且后续没有持续重现。
因此目前把这次 520 当作一次偶发的源站/CDN 请求异常,没有把它和 nonce 修复直接关联起来。
十一、继续测试其他 English 文章
随后又选择另外一篇 English 文章继续验证。
同样可以看到:
HTTP 200
counted:true
这说明修复并不是只对某一个具体 Post ID 有效。
十二、中文文章计数也保持正常
为了确认兼容层没有对中文主站产生副作用,又测试了一篇中文文章。
结果同样:
HTTP 200
counted:true
因此没有发现兼容代码破坏其他正常计数路径。
十三、后台文章列表中的浏览量已经恢复
经过几篇文章连续测试以后,再次回到 WordPress 后台文章列表。
之前异常的浏览量已经开始正常增加。

到这里,最开始的 stale nonce 问题实际上已经基本解决。
顺手发现的第二个低频问题
在继续观察的过程中,我又遇到了一件有点奇怪的事情。
一篇刚发布的中文文章,我记得自己已经访问过几次,但在后台重新刷新文章列表以后,它的浏览量仍然显示为:
0
这让我一度怀疑:
前面的修复是不是还有遗漏?
于是继续检查。
十四、这一次不是 nonce,而是 Cookie 去重
打开文章并查看 REST 请求。
这一次:
HTTP 200没有再出现:
rest_cookie_invalid_nonce但是响应为:
{
"post_id": 27587,
"counted": false,
"storage": [],
"type": "post"
}
继续查看浏览器发出的 storage data:
{
"version": 1,
"session_id": "71075757-e541-4cb8-8f90-7c268cab8768",
"started_at": 1789646679,
"expires_at": 1789650279,
"visited": {
"post": [
27587
]
}
}其中:
started_at
→ 20:04:39
expires_at
→ 21:04:39正好相差 1 小时。
这与我的 Post Views Counter:
Count Interval = 1 hour完全一致。
所以当前这次:
counted:false本身并不是 Bug。
它只是说明:
当前浏览器在一小时内已经成功统计过这篇文章,因此重复访问不会再次增加浏览量。
十五、但为什么之前明明访问过,后台却还是 0?
真正奇怪的是:
如果浏览器已经认为这篇文章统计过一次,为什么后台此前还会显示 0?
于是继续检查 nginx access log:
grep -RniE \
'post-views-counter/view-post/27587' \
/data/wwwlogs /var/log/nginx \
2>/dev/null可以看到:
18:58:06 POST /view-post/27587 → 200
18:59:48 POST /view-post/27587 → 200
19:05:11 POST /view-post/27587 → 200
19:10:29 POST /view-post/27587 → 200
19:20:07 POST /view-post/27587 → 200
19:20:28 POST /view-post/27587 → 200
20:04:39 POST /view-post/27587 → 200
20:04:59 POST /view-post/27587 → 200根据响应大小和后续实际请求内容,可以看到比较明显的模式:
215 / 216 bytes
≈ counted:true + storage
85 bytes
≈ counted:false也就是说,18:58 左右其实已经成功产生过浏览量。
问题并不是访问没有发生。
十六、数据库暴露出了真正的异常
直接查询 Post Views Counter 使用的:
wp_post_views表以后,发现 27587 的状态是:
day = 3
week = 3
month = 3
year = 3
total = 1正常一次访问,会同时增加:
day
week
month
year
total所以正常应该类似:
3 / 3 / 3 / 3 / 3但现在却是:
3 / 3 / 3 / 3 / 1说明:
中间曾经有某个操作只修改了
total,但没有修改 day/week/month/year。
继续查看插件源码以后,正好发现了这样的代码路径:
pvc_update_post_views()它只会直接修改:
type = 4
period = total而不会同步 day/week/month/year。
十七、最终定位到 Gutenberg 编辑器中的旧浏览量覆盖
继续检查 Post Views Counter 的 Block Editor 代码。
Gutenberg 编辑器打开时,插件会读取当时的浏览量:
$count = pvc_get_post_views( $id );并保存到:
pvcEditorArgs.postViews假设刚发布时:
total = 0那么已经打开的 Gutenberg 编辑器就一直保存着:
postViews = 0随后,文章前台产生了两个新的真实访问:
0 → 1 → 2数据库已经变成:
2但一直没有刷新的 Gutenberg 页面仍然保存:
postViews = 0而插件的 block-editor.js 在文章保存时会调用:
/post-views-counter/update-post-views/并提交当前编辑器 state 中的:
post_views于是就可能形成:
刚发布:
0 / 0 / 0 / 0 / 0
两次真实访问:
2 / 2 / 2 / 2 / 2
编辑器仍保存旧值:
postViews = 0
再次保存文章:
2 / 2 / 2 / 2 / 0
后来再产生一次真实访问:
3 / 3 / 3 / 3 / 1最终正好和数据库实际情况一致。
十八、admin 子域日志也找到了对应请求
因为 WordPress 后台使用的是:
admin.shuijingwanwq.com所以继续检查 admin 子域 nginx 日志。
果然找到了:
18:57:51
POST /wp-json/post-views-counter/update-post-views/?id=27587
18:57:55
POST /wp-json/post-views-counter/update-post-views/?id=27587
19:04:57
POST /wp-json/post-views-counter/update-post-views/?id=27587而文章的:
post_modified恰好为:
2026-09-17 19:05:00两者时间完全吻合。
所以这个低频问题基本也已经解释清楚:
Gutenberg 编辑器打开以后,如果前台浏览量发生变化,而编辑器没有重新加载,那么再次保存文章时,Post Views Counter 可能使用编辑器最初读取到的旧浏览量覆盖最新的
total。
十九、为什么这个问题暂时没有继续修?
因为这个问题的触发条件比较特殊:
打开 Gutenberg 编辑器
↓
编辑页面一直保持打开
↓
期间前台产生新的浏览量
↓
随后再次保存文章而我自己发布文章以后再次修改的频率并不高。
即使偶尔损失一两次浏览量,对整体统计影响也比较有限。
相比之下,前面的 stale nonce 问题会持续影响正常访客的计数,所以那个必须修。
这个 Gutenberg 边界问题目前只记录下来,没有继续增加新的生产兼容代码。
后来重新产生有效访问以后,后台浏览量也恢复为了 1。

二十、第二天再次确认 English 浏览量正常变化
修复以后没有马上结束观察。
第二天再次查看 English 文章列表时,可以看到最近发布文章的浏览量已经继续变化。
例如最近几篇分别已经出现:
5
5
7
8
4
4
2
9
……已经不存在之前连续多篇文章长期保持:
0的情况。

这一步也比较重要。
因为它证明:
前面的成功并不只是人工测试过程中偶然计数了一两次,而是在后续真实访问中持续正常工作。
因此目前可以确认,最主要的 Post Views Counter stale nonce 问题已经解决。
二十一、最终结论
这次最主要的问题可以总结为:
Post Views Counter REST API
+
WordPress wp_rest nonce
+
长生命周期页面缓存三者之间发生了生命周期不匹配。
页面缓存可以存在几天,但是页面中嵌入的 REST nonce 在更早的时候就会失效。
于是:
缓存页面
↓
携带旧 nonce
↓
PVC REST 请求
↓
WordPress REST Cookie Authentication
↓
403 rest_cookie_invalid_nonce
↓
文章浏览量停止增长增加 MU-plugin 兼容层以后:
缓存页面
↓
浏览器仍发送旧 nonce
↓
确认这是 PVC view-post 请求
↓
确认 nonce 已失效
↓
生成当前有效 wp_rest nonce
↓
WordPress Core 正常完成 Authentication
↓
Post Views Counter 正常执行计数
↓
HTTP 200
↓
counted:true整个过程中没有:
- 关闭 W3 Total Cache;
- 缩短整个网站的页面缓存时间;
- 修改 Post Views Counter 插件源码;
- 切换 Post Views Counter 的计数模式;
- 修改 Cloudflare 配置。
只增加了一层作用范围非常有限的兼容处理。
二十二、这次排查中值得记录的几个经验
第一个是:
HTTP 403 并不一定意味着 CDN 或 WAF 拦截。
这一次 Cloudflare 工作正常,请求也已经到达 WordPress。
真正返回 403 的是:
WordPress REST Cookie Authentication所以遇到 REST API 403 时,首先还是应该检查具体响应内容。
如果看到:
rest_cookie_invalid_nonce排查方向就已经和普通 WAF 拦截完全不同。
第二个是:
动态认证信息进入长期静态缓存时,需要格外小心。
这一次真正的问题并不是:
页面缓存坏了也不是:
REST API 坏了而是:
生命周期较短的 nonce被放入:
生命周期明显更长的缓存页面两个生命周期不匹配,最终自然会产生问题。
第三个则是这次顺手发现的 Gutenberg 边界问题:
后台编辑器里缓存的旧状态,也有可能覆盖前台已经发生变化的实时数据。
它影响不大,所以目前没有专门修复,但至少以后再遇到类似现象,就已经知道从哪里开始排查。
目前这个问题就先记录到这里。
主要的浏览量计数故障已经解决。
经过后续继续观察,English 子站最近文章的浏览量都能够正常变化,也没有再出现之前连续多篇新文章长期保持 0 的情况。
WordPress 网站维护、性能优化与博客运营咨询
本站已持续运营超过 10 年,累计发布 1000+ 篇原创技术文章,长期实践 WordPress 网站建设、CDN / Cloudflare 配置、缓存优化、Google SEO、广告变现和多语言网站运营。
如果你的 WordPress 网站遇到访问慢、缓存异常、插件冲突、广告不显示、SEO 基础结构混乱、CDN 配置不确定等问题,可以联系我做一次远程技术排查。
适合以下用户:
✅ 个人博客站长
✅ WordPress 网站运营者
✅ 独立开发者与内容创作者
✅ SaaS 产品官网运营团队
✅ 希望优化网站速度与稳定性的站点
服务内容:
✅ WordPress 速度优化
✅ Cloudflare / CDN / 缓存配置排查
✅ 插件冲突与页面异常排查
✅ AdSense 广告显示问题排查
✅ SEO 基础结构检查
✅ 博客运营与商业化咨询
如需了解方案或交流相关问题,请直接联系我,并注明:WordPress 维护咨询。
联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com
