Node.js中如何正确创建Webhook调用时resolve的Promise
Webhook等待场景的Promise实现方案
你现在存储resolve的思路本身是这个场景下的标准实现方向,不存在本质错误,所谓"不用存resolve"的方案只是对逻辑做了封装,没有绕开核心的上下文关联逻辑——毕竟发起外部调用的出站请求、接收回调的入站Webhook是两个完全独立的HTTP请求,必须靠共享状态才能把两个异步流程关联起来。
当前实现的待修正问题
你现在的简化版代码有几个明显的生产环境隐患:
- 没有错误兜底:外部服务fetch失败、外部服务超时不回调时,对应的Promise永远不会决议,会导致调用方一直挂起
- 内存泄漏:Promise决议后没有删除Map里的条目,服务长时间运行后Map会持续膨胀
- 竞态问题:同一个id重复调用makeRequest时,后写入的resolve会覆盖前值,导致前一个Promise永远无法被决议
- 请求格式错误:原生fetch不支持直接传入JS对象作为请求体,不做JSON序列化和头设置的话,外部服务收不到正确的参数
优化后的可直接落地实现
可以用通用的Deferred模式把Promise的决议逻辑做标准化封装,把边界处理补全,代码逻辑会更清晰:
// 通用Deferred工具,把Promise的resolve/reject暴露到实例上 class Deferred<T = any> { public readonly promise: Promise<T>; public resolve!: (value: T | PromiseLike<T>) => void; public reject!: (reason?: any) => void; constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } const activeRequests = new Map<string, Deferred>(); // 回调超时兜底,根据业务调整时长 const REQUEST_TIMEOUT = 30 * 1000; function makeRequest(id: string) { // 同id已有进行中请求时直接返回已有Promise,避免重复发起调用 if (activeRequests.has(id)) { return activeRequests.get(id)!.promise; } const deferred = new Deferred(); // 超时自动失败兜底 const timeoutId = setTimeout(() => { deferred.reject(new Error(`Request ${id} wait webhook timeout`)); activeRequests.delete(id); }, REQUEST_TIMEOUT); // 统一资源清理逻辑 const cleanup = () => { clearTimeout(timeoutId); activeRequests.delete(id); }; // 不管Promise成功失败,只要决议就清理对应条目 deferred.promise.finally(cleanup); fetch("http://external.com/someComplexCalculation", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ webhook: `http://myservice.com/webhook/${id}` }), }) .then(async (res) => { if (!res.ok) { throw new Error(`Call external service failed, status: ${res.status}`); } // 外部服务接收成功后,才把请求标记为待回调状态 activeRequests.set(id, deferred); }) .catch((err) => { // 外部请求直接失败时立刻reject,不用等回调 deferred.reject(err); }); return deferred.promise; } // Webhook接收路由 expressApp.post("/webhook/:id", (req, res) => { const deferred = activeRequests.get(req.params.id); if (!deferred) { res.sendStatus(404); return; } // 把Webhook上报的结果作为resolve值返回给调用方 deferred.resolve(req.body); res.sendStatus(200); });
关于"不存储resolve"的说明
这个场景下不存在完全不需要共享存储的实现方式:
- 出站请求和入站Webhook是两个完全独立的TCP连接,分属两个独立的异步上下文,Node.js事件循环没有原生机制可以自动关联两个请求
- 不管是直接存resolve函数、存Deferred实例,还是换成EventEmitter做事件监听、用Redis做跨实例状态存储,本质都是维护
请求ID -> 待决议异步上下文的映射,核心逻辑和你现在的写法完全一致 - 如果服务是多实例部署,内存Map必须替换成Redis这类跨实例共享存储,否则Webhook请求落到其他实例时会找不到对应上下文,导致请求挂起。
内容的提问来源于stack exchange,提问作者djm181
相关产品推荐
相关产品推荐

