前两天有人在群里问:验证码接口调了,返回也是 200,但用户死活说没收到邮件。日志翻了一圈什么异常都没有,到底哪出了问题?
这事我碰到过,而且不止一次。根源不复杂,但第一次遇到确实会懵——因为你的代码没有任何报错,常规的排查手段全失灵。
200 到底代表什么
先把一个很基础的认知对齐:对于邮件、短信这类异步发送的接口,HTTP 200 只代表"请求已受理",不代表"已经送达"。
整个流程拆开看:
你的请求 → 接口校验参数 → 写入发送队列 → 返回 200↓后台异步取队列 → 连接邮件/短信网关 → 投递↓收件服务器接收 → 进收件箱 or 进垃圾箱 or 被拒
200 只是上半段走完了。下半段出任何问题——队列积压、网关超时、收件地址不存在、被对方的反垃圾策略拦截——你的代码都不会收到任何反馈。
这不是某个平台的特殊设计,几乎所有短信和邮件 API 都是这么干的。原因很简单:邮件投递本身就是一个秒级甚至分钟级的过程,不可能让 HTTP 请求一直挂着等结果。
一个具体的排查过程
拿我碰到的场景说。项目里的密码重置功能,用 HTTP 接口发验证码邮件,代码大概是这样:
import requestsdef send_reset_code(email, code):resp = requests.post("https://push.spug.cc/mail/K83N29xbcFG",json={"to": email,"scene": "密码重置","code": code,"minute": "15"},timeout=10)data = resp.json()if data["code"] == 200:print("发送成功")return Trueelse:print(f"发送失败: {data['msg']}")return False
用户反馈收不到邮件之后,我检查了几个地方:
- 业务日志里有没有报错 —— 没有,
data["code"]确实是 200 - 请求参数有没有问题 ——
to地址是对的,模板编码没写错 - 网络有没有异常 —— 服务器到接口的连通性正常
到这一步就卡住了。200 返回了,参数对了,网络通了,但邮件就是没到。
破局:request_id
其实返回体里一直有个字段我没在意:
{"code": 200,"msg": "请求成功","request_id": "N6KQ8m2W4xY7p9aB"
}
request_id 是这次发送任务在平台侧的唯一标识。拿它去发送记录里查,能看到这封邮件走到了哪一步:是还在队列里、已经投出去了、还是被退回了。
我那次查完发现状态是"已发送"——平台这边确实发出去了,收件服务器也接收了。问题出在最后一环:用户用的是企业邮箱,那封验证码邮件被企业邮箱的反垃圾策略扔进了垃圾箱。
让用户去垃圾箱翻了一下,果然在。
改进后的代码
这次踩坑之后我把代码改了两个地方。
第一,request_id 必须存下来。不存的话,用户报问题你连去哪查都不知道:
def send_reset_code(email, code):resp = requests.post("https://push.spug.cc/mail/K83N29xbcFG",json={"to": email,"scene": "密码重置","code": code,"minute": "15"},timeout=10)data = resp.json()if data["code"] == 200:request_id = data["request_id"]# 存到数据库,和用户 ID、邮箱关联起来db.save_send_log(user_email=email,request_id=request_id,send_type="reset_password")return request_idelse:raise Exception(f"邮件发送请求失败: {data['msg']}")
第二,前端给用户加了一句提示:"如果没收到,请检查垃圾邮件文件夹"。这句话看着不起眼,但确实挡掉了大部分客诉。
排查的固定套路
碰多了之后,我现在的排查流程已经定型了:
第一步:确认请求到没到。 看自己业务系统的日志,找到这次请求的 request_id。如果连 200 都没返回,那就是参数错了或者网络不通,先查这一层。
第二步:拿 request_id 查发送状态。 去平台的发送记录页面或者调查询 API,看这条消息的实际投递状态。常见的几种情况:
| 状态 | 说明 | 怎么处理 |
|---|---|---|
| 排队中 | 还没轮到,通常几秒内会发 | 等一会儿再查 |
| 已发送 | 平台已投递,收件服务器已接收 | 让用户查垃圾箱 |
| 发送失败 | 收件地址不存在或被拒收 | 确认邮箱地址是否正确 |
| 被退回 | 对方邮件服务器拒绝 | 看退回原因,通常是频率或信誉问题 |
第三步:如果显示"已发送"但用户还是没找到。 九成是被垃圾邮件过滤器吃了。企业邮箱(特别是 Exchange 和自建邮件服务器)的过滤策略比个人邮箱激进得多。有些甚至不会放进垃圾箱,直接静默丢弃。
这种情况真没什么好办法,只能让邮箱管理员把发件域加白名单。
批量发送的坑
如果你用的是批量发送(to 参数逗号分隔多个地址,邮件最多 10 个),有个细节要注意:其中一个地址格式不对可能导致整个请求失败,也可能只影响那一个地址。
我的做法是在业务层先做格式校验,把明显不合法的地址过滤掉再发:
import redef validate_email(email):pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'return bool(re.match(pattern, email.strip()))def send_batch(emails, scene, code, minute):valid = [e for e in emails if validate_email(e)]if not valid:raise ValueError("没有合法的邮箱地址")resp = requests.post("https://push.spug.cc/mail/K83N29xbcFG",json={"to": ",".join(valid),"scene": scene,"code": code,"minute": minute},timeout=10)return resp.json()
安全方面
模板编码就是你的调用凭证,相当于 API Key。几个要注意的地方:
- 不要写在前端代码里,不要提交到公开仓库,放环境变量或配置中心
- 服务端调用,如果服务器 IP 固定,配一下 IP 白名单
- 怀疑泄露的时候去平台重置编码,旧的会立刻失效
另外一点,code=200 只是请求受理成功,要确认最终送达状态得用 request_id 去查。这条在业务对账、发送成功率统计的场景下尤其重要——直接拿 200 的比例当送达率,数字肯定虚高。
碰到类似问题的可以按上面的套路排查。邮件发送用的是 push.spug.cc,有免费额度可以测试。