05.03 · n8n 源码剖析:Webhook 接入层 Webhook Ingress

本文是专栏「n8n 工作流引擎剖析」第 05 章(组件深度剖析)的第 03/11 篇,承接上一篇《触发器注册中心 Trigger Registry》。讲的是从一个原始 HTTP 请求到调用 WorkflowRunner.run 之间发生的一切。
你在这里: 读完本文,你能从 Express 一路追踪请求直到第一个数据项,能解释各种响应模式,并能说清为什么 Webhook 节点会在执行记录存在之前就先运行。

缩写:HTTP (Hypertext Transfer Protocol,超文本传输协议)、CORS (Cross-Origin Resource Sharing,跨域资源共享)、ID (Identifier,标识符)、DB (Database,数据库)、JSON (JavaScript Object Notation,JavaScript 对象表示法)、V8(Google's JavaScript engine,谷歌的 JavaScript 引擎)。


角色回顾

负责: HTTP 请求 →(找到对应 webhook、加载已发布工作流、生成首个数据项、给出 HTTP 响应)。

掌握: 路由 → 数据行的映射(通过 WebhookService)、响应模式、请求/响应对象。

不做: 不遍历图;不决定跑在主进程还是工作进程;不持久化执行记录。

出现于: S3、S4、S5(以及 S6 末尾那个被延迟发出的回复)。

内部设计

#mermaid-svg-fmgg9j72TQCVuKiL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fmgg9j72TQCVuKiL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fmgg9j72TQCVuKiL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fmgg9j72TQCVuKiL .error-icon{fill:#552222;}#mermaid-svg-fmgg9j72TQCVuKiL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fmgg9j72TQCVuKiL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fmgg9j72TQCVuKiL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fmgg9j72TQCVuKiL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fmgg9j72TQCVuKiL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fmgg9j72TQCVuKiL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fmgg9j72TQCVuKiL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fmgg9j72TQCVuKiL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fmgg9j72TQCVuKiL .marker.cross{stroke:#333333;}#mermaid-svg-fmgg9j72TQCVuKiL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fmgg9j72TQCVuKiL p{margin:0;}#mermaid-svg-fmgg9j72TQCVuKiL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fmgg9j72TQCVuKiL .cluster-label text{fill:#333;}#mermaid-svg-fmgg9j72TQCVuKiL .cluster-label span{color:#333;}#mermaid-svg-fmgg9j72TQCVuKiL .cluster-label span p{background-color:transparent;}#mermaid-svg-fmgg9j72TQCVuKiL .label text,#mermaid-svg-fmgg9j72TQCVuKiL span{fill:#333;color:#333;}#mermaid-svg-fmgg9j72TQCVuKiL .node rect,#mermaid-svg-fmgg9j72TQCVuKiL .node circle,#mermaid-svg-fmgg9j72TQCVuKiL .node ellipse,#mermaid-svg-fmgg9j72TQCVuKiL .node polygon,#mermaid-svg-fmgg9j72TQCVuKiL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fmgg9j72TQCVuKiL .rough-node .label text,#mermaid-svg-fmgg9j72TQCVuKiL .node .label text,#mermaid-svg-fmgg9j72TQCVuKiL .image-shape .label,#mermaid-svg-fmgg9j72TQCVuKiL .icon-shape .label{text-anchor:middle;}#mermaid-svg-fmgg9j72TQCVuKiL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fmgg9j72TQCVuKiL .rough-node .label,#mermaid-svg-fmgg9j72TQCVuKiL .node .label,#mermaid-svg-fmgg9j72TQCVuKiL .image-shape .label,#mermaid-svg-fmgg9j72TQCVuKiL .icon-shape .label{text-align:center;}#mermaid-svg-fmgg9j72TQCVuKiL .node.clickable{cursor:pointer;}#mermaid-svg-fmgg9j72TQCVuKiL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fmgg9j72TQCVuKiL .arrowheadPath{fill:#333333;}#mermaid-svg-fmgg9j72TQCVuKiL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fmgg9j72TQCVuKiL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fmgg9j72TQCVuKiL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fmgg9j72TQCVuKiL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fmgg9j72TQCVuKiL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fmgg9j72TQCVuKiL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fmgg9j72TQCVuKiL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fmgg9j72TQCVuKiL .cluster text{fill:#333;}#mermaid-svg-fmgg9j72TQCVuKiL .cluster span{color:#333;}#mermaid-svg-fmgg9j72TQCVuKiL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fmgg9j72TQCVuKiL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fmgg9j72TQCVuKiL rect.text{fill:none;stroke-width:0;}#mermaid-svg-fmgg9j72TQCVuKiL .icon-shape,#mermaid-svg-fmgg9j72TQCVuKiL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fmgg9j72TQCVuKiL .icon-shape p,#mermaid-svg-fmgg9j72TQCVuKiL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fmgg9j72TQCVuKiL .icon-shape .label rect,#mermaid-svg-fmgg9j72TQCVuKiL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fmgg9j72TQCVuKiL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fmgg9j72TQCVuKiL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fmgg9j72TQCVuKiL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 存在 workflowData
没有 workflowData
Express:app.all('/webhook/*path')
createWebhookHandlerFor(liveWebhooks,'webhook')
WebhookRequestHandler.handleRequest

方法检查 · CORS

OPTIONS→204
LiveWebhooks.executeWebhook
findWebhook(path, method)

缓存 → DB 静态 → DB 动态
加载已发布版本

→ new Workflow(...)
sanitizeWebhookRequest

(除非节点在认证白名单中)
WebhookHelpers.executeWebhook
evaluateResponseOptions, parseRequestBody
invokeWebhook → WebhookService.runWebhook → node.webhook(ctx)
handleImmediateWebhookResponse
prepareExecutionData → WorkflowRunner.run(...)
直接响应,不创建执行
延迟的 'onReceived' 回复

图注:接入层的处理流水线------注意节点自己的 webhook() 是先执行的,它甚至可能决定根本不启动任何执行。

  1. 路由。 AbstractServer 把 app.all('/<endpointWebhook>/*path', createWebhookHandlerFor(liveWebhooks, 'webhook')) 挂载在请求体解析器之前 ,这样 Webhook 节点才能自己流式处理二进制请求体(cli/src/abstract-server.ts 第 240--252 行)。表单(Form)以及等待中的 webhook/表单恢复接口有对应的兄弟路由;测试 webhook 由 TestWebhooks 在 webhook-test 前缀下提供服务。

  2. 处理器。 WebhookRequestHandler.handleRequest 拒绝不支持的方法,只在存在 origin 请求头时才应用 CORS,用 204 响应 OPTIONS,然后把请求交给 webhookManager.executeWebhook。错误会被转换成 HTTP 错误响应(未知的 webhook → WebhookNotFoundError)。

  3. 查找。 WebhookService.findWebhook → 静态缓存(webhook:${method}-${path})→ 静态数据库记录 → 动态路径探测(路径中带 :param 段的情况;路径参数会被复制进 request.params)。

  4. 加载已发布的版本。 loadWebhookExecutionData 使用 workflow.activeVersion(或者在某个开关后面的新发布服务)------nodes/connections 来自已发布的版本,绝不是草稿。构建出一个 Workflow;getBase(...) 创建 additionalData(凭据辅助对象、钩子占位、设置、生产调用下的 userId = 发布者)。

  5. 表达式隔离实例------仅在需要时才创建。 webhookPhaseNeedsIsolate 会跳过创建 V8 隔离实例,前提是满足一个很常见的场景:Webhook 节点 v2 及以上版本、参数是静态的、描述字段能原生解析。只要有任何一点无法证明是静态的,就会去获取一个隔离实例。

  6. WebhookHelpers.executeWebhook (webhook-helpers.ts 第 804 行):

    • 通过 evaluateResponseOptions 解析出 responseMode 等;不支持的模式 → HTTP 500。支持的模式有:onReceived、lastNode、responseNode、formPage、streaming、hostedChat;
    • parseRequestBody;
    • invokeWebhook → WebhookService.runWebhook(webhook.service.ts 第 646 行)构建一个 WebhookContext,调用节点的 webhook()。这一步出错会被上报,并以一个通用错误作答;不会创建任何执行记录。
  7. 由节点来决定。 Webhook 节点的 webhook()(Webhook.node.ts 第 225 行)会检查 IP 白名单、机器人过滤、认证方式(基础认证/请求头/JWT/n8n OAuth)、一个可选的"仅在满足条件时运行"表达式,然后构建出:

    ts 复制代码
    { json: { headers: req.headers, params: req.params, query: req.query, body: req.body } }

    并返回 { webhookResponse, workflowData: [[item]] }。如果认证失败,它会自己写出 403/401,并返回 { noWebhookResponse: true }(没有 workflowData → 不会创建执行)。

  8. 响应模式 (决定调用方在等什么):

    模式 调用方会收到......
    onReceived(默认) 一旦执行记录存在,就立刻收到一个 JSON 回复:如果配置了 responseData 就用它,否则是 { "message": "Workflow was started" }
    lastNode 运行结束时最后一个执行节点的输出
    responseNode Respond to Webhook 节点发送的任何内容
    streaming 运行过程中的分块流(sendChunk 钩子)
    formPage / hostedChat Form / Chat 触发器对应的界面页面流程
  9. 启动运行。 prepareExecutionData 构建出初始的 IRunExecutionData------关键是 executionData.nodeExecutionStack = [{ node: <起始节点>, data: { main: <workflowData> }, source: null }]------然后:

    ts 复制代码
    executionId = await Container.get(WorkflowRunner).run(
      runData, /*loadStaticData*/ true,
      /*realtime*/ !didSendResponse && !shouldDeferOnReceivedResponse,
      existingExecution /* 只有在恢复一次 Wait 时才有值 */, responsePromise);

    对于 onReceived 模式,回复会延迟到 run 返回之后 ,这样 responseData 表达式里就能用上 $execution.id(webhook-helpers.ts 约第 1262--1290 行)。

交互关系

对象 契约
触发器注册中心 读取它写入的数据行;启动时会预先填充静态缓存(populateCache)
节点 调用 webhook(ctx);期望得到 IWebhookResponseData(workflowData、webhookResponse、noWebhookResponse)
工作流运行器(下一篇) run(IWorkflowExecutionDataProcess, ...) → 返回一个执行 ID
队列模式 对于 responseNode/lastNode,工作进程会把响应转发回这个进程(伸缩队列是本系列后续文章)

⚓ 回到示例 ------ S3 → S5

POST /webhook/orders,内容为 {"customer":"ACME","amount":100}(Content-Type: application/json):

  1. createWebhookHandlerFor 拼出 params.path = orders。调试日志:Received webhook "POST" for path "orders"。

  2. findWebhook('POST','orders') → 缓存未命中 → 数据库命中 (orders, POST) → 写入缓存。

  3. 加载已发布版本;webhookPhaseNeedsIsolate 返回假(Webhook v2、参数是静态的)→ 不构建隔离实例。

  4. runWebhook 调用 Webhook.webhook(ctx);没有配置认证,validateAuth 通过;结果:

    json 复制代码
    { "workflowData": [[ { "json": {
        "headers": { "content-type": "application/json", "...": "..." },
        "params": {}, "query": {},
        "body": { "customer": "ACME", "amount": 100 } } } ]] }
  5. prepareExecutionData 把这个数据项放进初始栈里;WorkflowRunner.run 被调用(S6 开始)。它返回例如 "1042";直到这时接入层才发出:

    http 复制代码
    HTTP/1.1 200 OK
    {"message":"Workflow was started"}

失败行为

失败情形 结果
找不到 (path, method) 对应的记录 抛出 WebhookNotFoundError → 404(错误信息里会列出这个路径已注册的其他方法)
webhook() 中认证失败 节点自己写出 401/403,返回 noWebhookResponse;不创建执行
webhook() 抛出异常 上报给错误报告器;返回一个通用错误响应;不创建执行
不支持的响应模式 500,提示 The response mode '...' is not valid!
调用方断开连接 对 onReceived 模式没有影响------执行记录已经创建好了
WorkflowRunner.run 抛出异常(例如执行前被阻断) 异常会一路传到 HTTP 响应;PreExecuteBlockedError 会在运行器里被解包

下一篇:《工作流运行器 Workflow Runner》,讲清楚一次执行是怎么被创建、又被决定跑在哪里的。


📚 返回专栏目录

相关推荐
加班循环3 小时前
前端嵌入低代码页面:iframe与微前端的取舍
低代码
内存过载3 小时前
流程实例状态追踪:事件溯源模式的应用
低代码
像素是个问题3 小时前
慢查询排查实战:业务报表SQL优化路径
低代码
IT研究所20 小时前
AI-ITR平台如何减少客户问题反复升级?
大数据·运维·人工智能·低代码·自然语言处理·安全架构·企微
guslegend1 天前
需求分析和架构设计:做什么,如何做
低代码·需求分析·架构设计·ssr·前端架构
三号路口1 天前
低代码平台的扩展机制:插件化架构怎么设计
低代码
百数平台1 天前
百数照片知识库配置指南:图片上传、OCR 识别与智能 / 人工标注全流程说明
低代码·ai·ocr
百数平台1 天前
百数表单知识库配置指南:对接低代码业务表单、字段自动映射与实时同步全说明
低代码·ai
百数平台2 天前
百数AI智能体基础开发流程:从创建、设计、发布到表单自动回填(含JSON输出与apaas配置)
低代码