跳到主要内容
一个给 webhook 兜底的隧道:`trydoorbell/doorbell`

一个给 webhook 兜底的隧道:`trydoorbell/doorbell`

小周
小周

· 阅读约 4 分钟

昨晚刷 Hacker News 刷到这个,点进去就蹲了半个多小时。

trydoorbell/doorbell,一句话:一个帮 webhook 买延迟险的隧道。它不只是在笔记本和公网之间捅个洞——要是没有客户端在收,它会先把请求存起来,等你重连再补发。

干什么用的,说具体点:你本地在调一个 webhook 接收端,隧道把公网请求转进来。但笔记本合上了、午休断电了、甚至忘了关隧道直接走人了,这期间进来的请求全丢。

GitHub 文档自己写得很明白:webhook 投递失败不会自动重试。Stripe 测试模式好一点,几小时内补个大约三次。Shopify 更狠,四小时内重试大约八次,一直失败就把整个订阅删了。所以你要是有一搭没一搭地本地开发,一顿午饭的功夫,这段时长的 webhook 全部报销。

我以前的办法是弄台带公网 IP 的临时服务器,挂个简单的接收脚本,请求原样 dump 成文件,回来手动比对。能用,糙得不行。Doorbell 把这个过程直接做成了产品功能:它有个公共网关,只要先占一个保留隧道名,哪怕现在没有任何客户端连接,它都会先回一个 202——收到,存进 Postgres 了。等你重连,它按最老的优先把请求一条条投给你,每条都标着在队列里等了多久。

这个"等了多久"我很喜欢。不是一堆过期请求一股脑砸过来就完事,它把"这些请求在黑暗里待了多久"如实交到你手上,你自己判断先后缓急。

有个语义细节,我第一眼没想明白,后来想通了觉得设计得很干净:这个 202 根本不是应用处理成功的信号。 它代表的是"网关收下这份请求、已经存数据库了"。你的应用到底有没有能力处理它,那是另一回事。说白了,隧道承担的是存储义务,不是处理义务。想通这一层,整个工具的行为逻辑就顺了。

签名这事处理得也诚实:延迟投递的 webhook 会去掉签名头,打上一个 X-Doorbell-Replay 头。因为它知道这些请求是被延迟过、重放过的,签名验不过很正常——这是你的接收端需要主动去处理的问题,它不假装没这事儿。

投递冲突的处理是个很土的办法:一条 SQL 把请求标记为"已被领走",谁抢到这一行谁投递。单实例场景下这就是最对的做法,不用搞什么分布式锁。

有个实现细节我挺在意:它用 yamux 做了一条多路复用的出站连接,配合 Go 的 httputil.ReverseProxy,省掉每次投递重新拨号的开销。原来的"网络拨号"变成了"从一条已经铺好的管道上再接一口",这个思路技术上简洁,资源占用也低。

但实话实说,这项目还在早期。作者拿它调试自己写的 AI agent,跑在 Zerops 上。部署需要一个常驻进程跑网关,加上 Postgres 和 Valkey,serverless 那种环境直接跑不起来。我本地完整摸了一圈,没上过生产,这个先声明。

摸的过程中踩到两个坑,反而让我对它更有好感。

第一个:作者一开始给隧道创建和查看请求体共用一个 token,拆成客户端 token 和管理员 token 是后来的事。另一个更有意思——有个查 token 的权限校验,在没配置 client token 的时候会把 query token 当成有效 token 接受。这个坑是作者自己发现的,发现得还算及时,补了个测试防住了。

这种"我也踩过这种低级坑"的诚实,比通篇吹设计多完美的 README 可信得多。我刷项目一贯不爱看那种全优生式的自述。

上手要做的第一件事是先占一个保留隧道名,给个不存在的名字会直接 404——防匿名滥用存储。存储上限是每隧道 200 条、每条 1MB,超了丢最老的。简单粗暴,够用。这是它自己定的规则:你不连,我就等你,但只等 200 条,再多就丢掉。

其实几个主流的 webhook 隧道工具,要么根本没有延迟重放这功能,要么这功能按功能收费。它给的是完整自托管方案,代码量还极少。整个设计透着"够用就好"四个字。

这项目让我想起那种"标准件之外的常用件":不是最大众的工具,但你真遇到那个场景,会觉得它就是对的答案。自己开发环境里被折腾出来的东西,用起来最顺手——问题够具体,方案才够明确。

要是你也在本地倒腾 webhook,尤其是那种"设备不在线,回家才能续上"的开发节奏,它值得从候选池里捞出来试一试。我的建议:先挂个 star 蹲更新,真用到再拉下来,别急着上生产。

候选池里还存着一个同类但场景更重的框架,下次翻它牌子。

能不能打,你自己拉下来跑一圈最准。