飞书机器人回调本地调不通?用 cpolar 打通 HTTPS 事件订阅链路
调飞书机器人或飞书应用事件订阅时,很多人第一步就卡在“请求地址校验失败”。本地服务明明已经启动,curl http://127.0.0.1:3000/feishu/events也能返回结果,但把地址填进飞书开放平台后,平台就是访问不到。
这不是代码一定写错了,而是本地调试天然隔着一层网络边界。飞书平台的事件回调由云端服务器发起,它不会运行在你的电脑上,也无法直接访问你机器里的localhost、127.0.0.1或公司 Wi-Fi 下的内网 IP。再加上事件订阅通常要求公网可访问的 HTTPS URL,本地 HTTP 服务就更不可能直接通过校验。
这篇文章用一个最小回调服务把链路跑通:本地启动 Node.js 或 FastAPI 接收飞书事件,通过 cpolar 把本地端口映射成公网 HTTPS 地址,再在飞书开放平台完成 URL 校验、事件回调配置和日志排查。它适合开发、联调和验收,不要把临时隧道当成长期生产入口。
为什么本地事件订阅总是调不通
飞书事件订阅的调用方向很重要:不是你的电脑去请求飞书,而是飞书平台在发生事件后主动 POST 到你配置的回调 URL。比如用户给机器人发消息、群聊里新增成员、审批状态变化,平台都会按你订阅的事件类型向目标地址推送 JSON 数据。
如果目标地址写成下面这些,基本都会失败:
http://127.0.0.1:3000/feishu/events http://localhost:3000/feishu/events http://192.168.1.23:3000/feishu/events原因很直接:127.0.0.1对飞书服务器来说是飞书服务器自己,不是你的电脑;192.168.x.x是内网地址,公网服务器通常无法路由进来;普通 HTTP 地址也可能不满足平台对安全回调地址的要求。你本地自测成功,只能说明服务在本机可用,不能证明飞书云端能访问。
cpolar 的作用就是补上“公网 HTTPS 入口”这一段。它在你的电脑和 cpolar 云端之间建立隧道,外部访问https://xxxx.cpolar.top/feishu/events时,请求会被转发到你本地的http://127.0.0.1:3000/feishu/events。这样飞书看到的是一个公网 HTTPS URL,你本地仍然可以保留开发环境和日志。
先写一个最小 Node.js 回调服务
下面先用 Node.js 写最小服务。它包含三个能力:健康检查、飞书 URL 校验、普通事件回调日志。为了便于理解,示例先把签名校验写成函数,开发时可以打开严格校验;如果你还在做第一轮连通性排查,也可以先只打印请求头和原始 body。
新建目录并安装依赖:
mkdir feishu-cpolar-callback-demo cd feishu-cpolar-callback-demo npm init -y npm install express创建server.js:
const crypto = require("crypto"); const express = require("express"); const app = express(); const PORT = Number(process.env.PORT || 3000); const ENCRYPT_KEY = process.env.FEISHU_ENCRYPT_KEY || ""; app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString("utf8"); } })); function safeCompareHex(a, b) { const left = Buffer.from(a || "", "utf8"); const right = Buffer.from(b || "", "utf8"); if (left.length !== right.length) return false; return crypto.timingSafeEqual(left, right); } function verifyFeishuSignature(req) { const timestamp = req.header("X-Lark-Request-Timestamp") || ""; const nonce = req.header("X-Lark-Request-Nonce") || ""; const signature = req.header("X-Lark-Signature") || ""; if (!ENCRYPT_KEY) { return { ok: true, reason: "signature skipped in local demo" }; } const base = timestamp + nonce + ENCRYPT_KEY + (req.rawBody || ""); const expected = crypto.createHash("sha256").update(base).digest("hex"); const ok = safeCompareHex(expected, signature); return { ok, reason: ok ? "matched" : "signature mismatch" }; } app.get("/health", (_req, res) => { res.json({ ok: true, service: "feishu-callback-demo", time: new Date().toISOString() }); }); app.post("/feishu/events", (req, res) => { console.log("\n[feishu callback]", new Date().toISOString()); console.log("headers:", { timestamp: req.header("X-Lark-Request-Timestamp"), nonce: req.header("X-Lark-Request-Nonce"), signature: req.header("X-Lark-Signature") }); console.log("body:", JSON.stringify(req.body, null, 2)); const body = req.body || {}; if (body.type === "url_verification" && body.challenge) { return res.status(200).json({ challenge: body.challenge }); } const signature = verifyFeishuSignature(req); if (!signature.ok) { console.warn("signature failed:", signature.reason); return res.status(401).json({ code: 401, msg: "invalid signature" }); } return res.status(200).json({ code: 0, msg: "success" }); }); app.listen(PORT, "127.0.0.1", () => { console.log(`local callback listening on http://127.0.0.1:${PORT}`); });启动服务:
node server.js先在本机确认服务可用:
curl http://127.0.0.1:3000/health curl -X POST http://127.0.0.1:3000/feishu/events \ -H 'Content-Type: application/json' \ -d '{"type":"url_verification","challenge":"local-test"}'第二个命令应该返回:
{"challenge":"local-test"}这一步只证明本地服务逻辑没问题。要让飞书开放平台访问到它,还需要公网 HTTPS 地址。
如果你更习惯 FastAPI
Python 项目可以用 FastAPI 起一个同样的最小回调服务。目录和依赖如下:
mkdir feishu-cpolar-fastapi-demo cd feishu-cpolar-fastapi-demo python -m venv .venv source .venv/bin/activate pip install fastapi uvicorn创建main.py:
import hashlib import hmac import json import os from datetime import datetime from fastapi import FastAPI, Request from fastapi.responses import JSONResponse app = FastAPI(title="Feishu Callback Demo") ENCRYPT_KEY = os.getenv("FEISHU_ENCRYPT_KEY", "") def verify_feishu_signature(headers, raw_body: bytes) -> bool: if not ENCRYPT_KEY: return True timestamp = headers.get("x-lark-request-timestamp", "") nonce = headers.get("x-lark-request-nonce", "") signature = headers.get("x-lark-signature", "") base = timestamp + nonce + ENCRYPT_KEY + raw_body.decode("utf-8") expected = hashlib.sha256(base.encode("utf-8")).hexdigest() return hmac.compare_digest(expected, signature) @app.get("/health") async def health(): return {"ok": True, "service": "feishu-callback-demo", "time": datetime.now().isoformat()} @app.post("/feishu/events") async def feishu_events(request: Request): raw_body = await request.body() body = json.loads(raw_body.decode("utf-8") or "{}") print("\n[feishu callback]", datetime.now().isoformat()) print("headers", dict(request.headers)) print("body", json.dumps(body, ensure_ascii=False, indent=2)) if body.get("type") == "url_verification" and body.get("challenge"): return {"challenge": body["challenge"]} if not verify_feishu_signature(request.headers, raw_body): return JSONResponse(status_code=401, content={"code": 401, "msg": "invalid signature"}) return {"code": 0, "msg": "success"}启动:
uvicorn main:app --host 127.0.0.1 --port 3000Node.js 和 FastAPI 二选一即可。真实项目里可以把 URL 校验和事件处理放在同一路由,因为飞书平台校验地址时也是向你配置的事件订阅 URL 发请求。
用 cpolar 映射成本地 HTTPS 回调入口
本地服务监听在127.0.0.1:3000后,打开一个新的终端启动 cpolar 隧道:
cpolar http 3000启动后,控制台会显示一个公网地址,形态通常类似:
https://abcd-xxx.cpolar.top把事件回调路径拼上去,就是要填到飞书开放平台的 URL:
https://abcd-xxx.cpolar.top/feishu/events在填到飞书之前,先用外部 HTTPS 地址自测一次:
curl https://abcd-xxx.cpolar.top/health curl -X POST https://abcd-xxx.cpolar.top/feishu/events \ -H 'Content-Type: application/json' \ -d '{"type":"url_verification","challenge":"cpolar-test"}'如果这里能返回cpolar-test,同时本地终端能看到请求日志,就说明“公网 HTTPS -> cpolar -> 本地端口”这条链路已经通了。后面飞书校验失败时,就可以把问题缩小到平台配置、请求格式、签名或应用权限上,而不是继续怀疑本地端口不可达。
在飞书开放平台配置事件订阅
进入飞书开放平台应用后台后,常见配置路径是“事件订阅”或“事件与回调”。不同应用类型的菜单名称可能略有差异,但核心步骤基本一样:
- 开启事件订阅能力。
- 将请求地址填写为
https://你的-cpolar-域名/feishu/events。 - 保存时平台会发起 URL 校验请求。
- 本地服务收到
url_verification类型请求后,原样返回challenge。 - 校验通过后,再选择需要订阅的事件,例如接收消息、群聊事件、审批事件等。
- 根据事件类型补齐应用权限,并发布或重新安装应用。
URL 校验最容易犯的错误是返回格式不对。飞书发来的请求体里会包含challenge字段,你的服务需要返回 JSON:
{ "challenge": "平台发来的 challenge 字符串" }不要返回字符串本身,也不要包在data字段里。HTTP 状态码也应该是200。如果你的服务返回302、404、500或超时,平台通常都会认为校验失败。
另一个常见问题是路径不一致。你本地代码监听的是/feishu/events,飞书后台就必须填写完整路径。如果只填 cpolar 根域名,平台请求会打到/,自然不会进入回调处理函数。
签名、Challenge、状态码和日志怎么排查
事件订阅调试按顺序排查,不要一上来就改业务代码。
第一步看 cpolar 隧道是否在线。访问/health能返回 JSON,说明公网入口和本地端口是通的;如果/health都失败,先检查 cpolar 进程、本地服务端口和防火墙。
第二步看本地终端有没有日志。飞书点击保存或重新校验时,本地服务应该打印请求头和请求体。如果 cpolar 有访问记录但本地没有日志,可能是本地端口写错了;如果本地完全没有任何访问,可能是飞书后台 URL 填错,或者 cpolar 地址已经变更。
第三步看challenge返回。URL 校验阶段不要做复杂业务处理,收到type=url_verification就立即返回challenge。校验请求不是普通事件,不需要你把它当成消息处理。
第四步看状态码。飞书事件回调通常希望你的服务快速返回成功响应。示例里普通事件返回200和{"code":0,"msg":"success"},这能避免平台反复重试。耗时任务不要在回调请求里同步执行太久,应该先记录事件 ID 或消息 ID,再交给队列、后台任务或异步流程处理。
第五步看签名。生产环境一定要校验飞书签名,避免任何人伪造请求打到你的回调地址。调试第一轮可以先打印请求头和 body,确认字段存在后再打开严格校验。注意签名计算依赖时间戳、随机串、密钥和原始请求体,如果中间件提前改写了 body,或者你用格式化后的 JSON 重新计算,就可能导致验签失败。
第六步看事件权限。URL 校验通过不代表事件一定会推送。比如订阅“接收消息”事件后,应用还需要对应权限,并且机器人可能需要被拉入群聊或安装到指定范围。没有权限时,平台不会按你预期推送事件,或者推送内容字段不完整。
安全收口:临时入口不要长期裸奔
cpolar 很适合开发联调,但临时公网入口也意味着你的本地服务被外部网络访问到了。调试时至少做好这些收口:
- 只暴露必要路由,不要把整个后台管理系统、数据库控制台、调试面板一起映射出去。
- 本地服务监听
127.0.0.1即可,让 cpolar 转发到本机端口,不需要额外监听0.0.0.0。 - 事件回调必须校验飞书签名,敏感操作还要做事件类型、应用 ID、租户信息和消息来源校验。
- 日志里避免打印 access token、refresh token、用户手机号、邮箱、消息全文等敏感信息。
- 联调结束后关闭 cpolar 隧道,并从飞书后台移除临时回调地址或切回正式域名。
- 不要把临时 cpolar 地址硬编码进仓库,也不要把密钥写进示例代码提交到公开仓库。
真实上线时,事件回调最好部署到稳定的 HTTPS 服务上,配合网关、鉴权、日志、告警和限流。cpolar 更适合“本地开发阶段快速让平台打进来”,用来确认协议、字段、权限和业务处理逻辑。
一个可复用的联调流程
我实际调这类飞书回调时,会按这个顺序做:
- 本地起最小服务,只实现
/health和/feishu/events。 - 本机
curl通过,确认 challenge 返回格式正确。 - 启动 cpolar,拿到 HTTPS 地址。
- 用 cpolar HTTPS 地址再
curl一遍,确认本地能看到日志。 - 把完整事件订阅 URL 填到飞书开放平台。
- 先让 URL 校验通过,再订阅具体事件。
- 触发真实事件,例如给机器人发一条测试消息。
- 根据本地日志检查请求头、事件类型、事件 ID、消息内容和响应状态码。
- 打开签名校验,确认验签通过。
- 联调完成后关闭隧道,把平台配置切回正式环境。
这个流程的好处是每一步都有明确的判断标准。失败时你知道该看哪里,而不是在飞书配置、本地代码、网络入口和权限之间来回猜。
总结
飞书机器人事件订阅本地调不通,核心原因通常不是框架问题,而是飞书云端无法直接访问你的本机地址。localhost、内网 IP、本地 HTTP 端口都不适合作为平台回调 URL。
用 cpolar 可以快速生成一个公网 HTTPS 入口,把飞书的 URL 校验和事件回调转发到本地 Node.js 或 FastAPI 服务。调试时重点关注四件事:challenge必须按格式返回,回调路径必须一致,普通事件要快速返回200,签名和权限要在链路通了之后逐步收紧。
最后再强调一次边界:cpolar 是开发联调工具,不是让本地服务长期裸露在公网的理由。用它把协议、字段、权限和业务逻辑验清楚,完成后关闭隧道,正式环境再迁移到稳定的 HTTPS 服务上,这样既能提高调试效率,也能把安全风险控制在合理范围内。