回调不通,八成不是代码问题。这篇按"连通性、验签、错误码"三层走,每层给可执行的验证命令。
第一层:先确认请求到没到你这里
别急着看代码。先看有没有请求进来。
从外网 curl。找一台不在你内网的机器执行:
python
curl -v -X POST https://your-domain.com/wx-api/webhook/ \
-H "Content-Type: application/json" \
-d '{"data":{"content":"test"}}'
看到 HTTP/1.1 200 说明链路通。看到 Connection refused 就是端口不通,看到 SSL certificate problem 就是证书有问题。
在服务器上抓包。如果 curl 通但回调不来,抓包看有没有请求到达:
sudo tcpdump -i any -n port 443 -A | grep -i "POST /wx-api"
没抓到,问题在平台侧或中间网络。抓到了,问题在你的应用。
常见的拦路虎:
| 现象 | 原因 |
|---|---|
| 外网 curl 超时 | 安全组/防火墙没放行 |
| 证书报错 | 证书链不全,或用了自签名 |
| 8080 能通、443 不通 | 只开了 HTTP,回调要求 HTTPS |
| 偶发 502 | 反向代理超时,应用处理太慢 |
回调必须公网可达、HTTPS、证书有效。自签名证书和 IP 直连都不行。
第二层:URL 验证过没过
回调地址配置时,平台会发一次验证请求,带 echostr。这一步不过,后面所有消息都不会推。
验证请求的硬性要求:
原样返回 echostr。不能包 JSON,不能加引号,不能带换行。
python
@app.route("/wx-api/webhook/", methods=["GET", "POST"])
def webhook():
echostr = request.args.get("echostr")
if echostr is None and request.is_json:
echostr = request.json.get("echostr")
if echostr:
return echostr, 200, {"Content-Type": "text/plain"} # 纯文本
...
三个容易踩的点:
-
返回
jsonify({"echostr": ...})→ 失败 -
返回时加了
\n→ 失败 -
只支持 POST,验证请求发的是 GET → 405
手动模拟验证请求:
curl -i "https://your-domain.com/wx-api/webhook/?echostr=abc123"
返回体应该只有 abc123,不能有别的字符。
第三层:验签对不对
验证过了、消息也来了,但返回 403,说明验签失败。
python
curl -i -X POST https://your-domain.com/wx-api/webhook/ \
-H "Content-Type: application/json" \
-H "X-Eyun-Signature: wrong-signature" \
-d '{"data":{}}'
预期返回 403。如果返回 200,说明你的验签根本没生效------比验签失败更危险。
验签三原则:原始字节、UTF-8、compare_digest。框架提前读过 body 是最常见的坑。
python
raw = request.get_data(cache=True) # 不是 request.json
expected = hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Eyun-Signature", "")):
return "forbidden", 403
第四层:API 返回的错误码怎么读
消息收到但发不出去,看响应码。企微的 code 有两种类型,处理方式完全不同。
| code | 类型 | 含义 | 处理 |
|---|---|---|---|
0 |
数字 | 成功 | 继续 |
-1 |
数字 | 业务失败,具体码在 message | 读 message,不重试 |
-301002 |
数字 | 限流 | 指数退避重试 |
字符串(如 "401") |
字符串 | 网关/鉴权错误 | 检查 Token 和路径 |
判断要按类型分:
python
code = resp.get("code")
if code == 0:
return resp["data"]
if isinstance(code, str):
raise AuthError(resp) # 别重试,先查凭证
if code == -301002:
backoff_and_retry()
把字符串码当业务码重试,会一直撞墙。这是排查时最常见的误判。
最后一种情况:其实收到了,只是你没看见
有两个"看起来没回调"的经典场景:
接口自己发的消息不回推 。你用 message/sendText 发出去的消息,平台不会推正文给你,只推一条 2001 已读回执。想看自己发的消息,得查历史消息接口。
只推了回执事件 。事件类型是已读回执,正文在另一条事件里。按 X-Eyun-Event 分流时,把回执事件单独处理,别丢。
一套可复用的排查顺序
-
外网 curl,确认 200
-
tcpdump,确认请求到达
-
手动请求 echostr,确认纯文本返回
-
故意发错签名,确认返回 403
-
发一条真实消息,看日志里 code 的类型
-
确认不是"自己发的消息"或"只收到回执"
六步走完,问题基本能定位到具体一层。
回调配置和错误码说明在开发文档里能查到,实例的密钥和回调地址在 Eyun 控制台的实例配置里改。