You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 21:21:38