企业微信 Webhook 接收不到消息?网络连通性、URL 签名验签与 API 返回错误码排查指南

回调不通,八成不是代码问题。这篇按"连通性、验签、错误码"三层走,每层给可执行的验证命令。

第一层:先确认请求到没到你这里

别急着看代码。先看有没有请求进来。

从外网 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 分流时,把回执事件单独处理,别丢。

一套可复用的排查顺序

  1. 外网 curl,确认 200

  2. tcpdump,确认请求到达

  3. 手动请求 echostr,确认纯文本返回

  4. 故意发错签名,确认返回 403

  5. 发一条真实消息,看日志里 code 的类型

  6. 确认不是"自己发的消息"或"只收到回执"

六步走完,问题基本能定位到具体一层。

回调配置和错误码说明在开发文档里能查到,实例的密钥和回调地址在 Eyun 控制台的实例配置里改。

相关推荐
薛之谦_1 小时前
JD Hotkey 项目实战:京东开源的热点探测与秒杀系统
java·安全
yuzhiboyouye1 小时前
@EnumValue和@JsonValue
java
IT大白鼠1 小时前
MCP 生态半年报从 Anthropic 的一份协议,到 AI 世界的「USB-C」
人工智能
TMT星球1 小时前
觅蜂联合发起具身评测平台RoboColiseum,GPT-6、Opus 5.5、Gemini 4同台决战!
人工智能·gpt
lucas_AI1 小时前
Kimi K3.1 Pro 发布:104B 激活参数追到 Opus 5.5 差 0.7 个点,还主动「删掉」了自己的网络攻击能力
人工智能
AIGC大时代1 小时前
AI 辅助题录筛选怎么选型终裁:金标准召回·漏检·批处理边界的能用/限场景/不能用验收
人工智能
ebok.1 小时前
低代码平台如何集成 AI:从开发侧到运行侧的技术实践与京微智枢方案
人工智能·低代码·ai·rxjava
larance1 小时前
[机器学习40讲]28-最简单的概率图:朴素贝叶斯
人工智能·机器学习
相隔一个图书馆的距离1 小时前
单测(三)Spring Boot 3.5.6 && @MockitoSpyBean / AOP / 循环依赖:静默吞错与运行期 NPE
java·spring boot·后端