表单页显示「提交成功」,后台翻遍没有记录,通知邮箱也没有动静。这三种现象分别属于不同的层:浏览器的请求发没发出去、应用有没有接住并落库、邮件有没有送到收件人手里。当成一个问题查,很容易在「刷缓存」和「换发信服务」之间来回猜。下面按层拆开,每层都写清楚该看哪里、命令怎么跑。
先在浏览器开发者工具的 Network 面板找到那条提交请求,看状态码和响应体。200 加一段 JSON 成功标记,才算前端这一层通过;403 多半是 REST 的 nonce 跟着页面缓存一起过期了,429 是被限速,499 或 502 是网关在应用返回之前就断了连接。
有些表单的「成功」提示是前端脚本直接弹的,请求失败了照样显示。判断办法很简单:把响应体贴出来看,或者直接对端点发一次请求。
curl -sS -o /dev/null -D - -X POST 'https://example.com/wp-json/your-form/v1/submit' \
-H 'Content-Type: application/json' \
--data '{"name":"selftest","email":"selftest@example.com"}'这条命令不带 nonce,正常情况下返回 403,说明路由存在、鉴权也在生效。返回 404 说明端点根本没注册——插件没启用,或者重写规则没刷新,wp rewrite flush 跑一次再看。返回 200 反而要警惕,等于鉴权没起作用,任何人都能往里灌数据。
浏览器报错只能说明它没收到响应,请求可能已经打到服务器又被挡回去了。先看 Web 服务器的 access log:
# 最近的表单类 POST
grep -E 'POST /(wp-json|wp-admin/admin-ajax\.php)' /var/log/nginx/access.log | tail -n 30
# 状态码分布,看有没有成片的 403 / 429 / 499
grep -E 'POST /(wp-json|wp-admin/admin-ajax\.php)' /var/log/nginx/access.log \
| awk '{print $9}' | sort | uniq -c | sort -rn日志里连 POST 都没有,问题在客户端或中间层:CDN 边缘节点、WAF 规则、浏览器插件、公司网络代理,逐个摘掉试。日志里全是 403,先区分是 WAF 拦的还是应用拦的——WAF 拦的请求通常不会进 PHP,PHP 的错误日志里找不到对应记录,而云上的 WAF 日志能查到命中的规则 ID。日志里 499 居多,说明 PHP 处理太慢、客户端先放弃了,去看 php-fpm 的慢日志:
tail -n 200 /var/log/php-fpm/error.log
grep -iE 'waf|mod_security|denied' /var/log/nginx/error.log | tail -n 50还有一种情况是请求到了、数据也进来了,只是没进你以为的那张表。多站点装 WordPress 时 $wpdb->prefix 带站点编号,去主站表里翻自然什么都没有。
后台有记录、邮件没来,说明前半段是通的,问题在发信。后台没记录但 access log 里有 200,说明处理函数跑过了,只是写库失败或者写到了别处——查 PHP 错误日志里有没有 SQL 报错、事务有没有回滚。两条链路混着看,最容易把「没入库」误判成「邮件被拦」。
应用层组装的收件人:To、CC、BCC 到底是谁,从代码或插件设置里确认一遍。别急着往后查,先把这一步的证据拿到——多数「某个同事收不到」的问题,收件人列表里本来就漏了他。
发信通道:SMTP 还是 HTTP API。SMTP 看邮件服务的 maillog,API 看返回的 message id 和状态字段。发信插件一般有自己的日志表或日志文件,先确认这条通知确实交给通道了。
收件网关:企业邮箱的隔离区、垃圾邮件判定、附件大小限制。这一段只能从收件方后台看,发信方看不到。
收件人规则:过滤器、别名、转发链。这一段最容易被忽略,也最容易出「只有我收不到」。
排查顺序按这个走,别跳步:
漏的通常是这几样,按顺序核:
p=reject 时,新服务如果没有对齐(From 域与 DKIM 的 d= 或 SPF 的 Return-Path 同域),会被直接拒收。验证记录用 dig 看,别只看服务商后台的绿勾:
dig +short TXT example.com | tr -d '"' | grep -i spf
dig +short TXT your-selector._domainkey.example.com
dig +short TXT _dmarc.example.com
# 粗略数一下 SPF 里嵌了多少 include
dig +short TXT example.com | grep -o 'include:[^ ]*' | wc -l4xx 是临时失败,队列里会重试,先别动配置;5xx 是永久失败,重试没有意义。
返回 | 含义 | 先查什么 |
|---|---|---|
5.1.1 | 收件人不存在 | 地址拼写、别名是否还有效 |
5.7.x | 策略拒收 | 黑名单、SPF/DKIM 对齐、内容判定 |
4.7.x | 临时限速或灰名单 | 发送频率、单次收件人数 |
5.2.2 | 邮箱满了 | 收件人侧,发信方无解 |
发信服务后台还会有两块要看:抑制名单和额度告警。抑制名单是硬退信攒出来的,不清掉的话同一地址永远发不出去;额度告警被忽略,超限之后的通知会静默失败。
同步发信失败就丢,是最常见的坑。WordPress 里可以挂住失败事件先落库,再用系统 cron 重试,别指望 WP-Cron——它靠访问触发,低流量站点可能一直不跑。
<?php
// 放进一个小插件里。先建表:created_at、recipients、subject、message、attempts
add_action( 'wp_mail_failed', function ( WP_Error $error ) {
$data = $error->get_error_data();
$data = is_array( $data ) ? $data : array();
$to = $data['to'] ?? array();
$to = is_array( $to ) ? implode( ',', $to ) : (string) $to;
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'mail_failures',
array(
'created_at' => gmdate( 'Y-m-d H:i:s' ),
'recipients' => $to,
'subject' => (string) ( $data['subject'] ?? '' ),
'message' => $error->get_error_message(),
'attempts' => 0,
),
array( '%s', '%s', '%s', '%s', '%d' )
);
} );
add_action( 'aum_retry_failed_mail', function () {
global $wpdb;
$table = $wpdb->prefix . 'mail_failures';
$rows = $wpdb->get_results(
"SELECT * FROM {$table}
WHERE attempts < 3 AND created_at > (UTC_TIMESTAMP() - INTERVAL 1 DAY)
ORDER BY id ASC LIMIT 20"
);
foreach ( $rows as $row ) {
$ok = wp_mail( explode( ',', $row->recipients ), $row->subject, $row->message );
$wpdb->query(
$wpdb->prepare( "UPDATE {$table} SET attempts = attempts + 1 WHERE id = %d", $row->id )
);
if ( $ok ) {
$wpdb->delete( $table, array( 'id' => $row->id ), array( '%d' ) );
}
}
} );系统 cron 里每五分钟跑一次,同时把默认的伪 cron 关掉:
# wp-config.php: define( 'DISABLE_WP_CRON', true );
*/5 * * * * cd /var/www/html && /usr/local/bin/wp cron event run aum_retry_failed_mail --quiet重试要防重复:原邮件可能其实已经发出去了,只是应用侧超时。用业务 ID 做幂等键传给发信服务,或者在表里记一个唯一键,重试前查一次。重试次数到顶还失败的,要能报警出来,否则队列就是个黑洞。
页面被整页缓存之后,表单里的 nonce 还是旧的,用户一提交就是 403,前台却可能显示成功。判断方法是看响应头里有没有缓存命中标记和 age,然后直连源站再提交一次:
curl -sS -o /dev/null -D - --resolve example.com:443:203.0.113.10 \
-X POST 'https://example.com/wp-json/your-form/v1/submit' \
-H 'Content-Type: application/json' --data '{"name":"selftest"}'绕过边缘节点后正常,问题就在缓存或 CDN 配置:把表单页和 REST 路径排除出缓存。WAF 这边要注意规则会不会误伤正常内容——公司介绍里带 URL、产品型号里带引号,都可能命中 SQL 注入或 XSS 规则被拦成 403,把字段内容减到最简再试一次就能定位。
会,路径有两条。一条是频率:机器人把提交量打上去,发信服务那边的发送速率被限,正常通知排到后面甚至被丢弃。另一条是声誉:如果垃圾提交触发了大量硬退信和投诉,发信域的评分会掉,之后所有通知都更容易进垃圾箱。
所以拦截要放在前面,别让垃圾请求走到发信那一步:加一个对用户不可见、机器人会填的隐藏字段,加一个「从打开表单到提交不少于两秒」的时间戳,再按 IP 做个每分钟的速率限制。通知邮件和营销邮件分开走不同的子域,营销那边的投诉率不会连累通知。
顺手想看一眼现有站点的抓取和索引状态,可以用站点检查工具。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。