本文是专栏「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() 是先执行的,它甚至可能决定根本不启动任何执行。
-
路由。
AbstractServer把app.all('/<endpointWebhook>/*path', createWebhookHandlerFor(liveWebhooks, 'webhook'))挂载在请求体解析器之前 ,这样 Webhook 节点才能自己流式处理二进制请求体(cli/src/abstract-server.ts第 240--252 行)。表单(Form)以及等待中的 webhook/表单恢复接口有对应的兄弟路由;测试 webhook 由TestWebhooks在webhook-test前缀下提供服务。 -
处理器。
WebhookRequestHandler.handleRequest拒绝不支持的方法,只在存在origin请求头时才应用 CORS,用204响应OPTIONS,然后把请求交给webhookManager.executeWebhook。错误会被转换成 HTTP 错误响应(未知的 webhook →WebhookNotFoundError)。 -
查找。
WebhookService.findWebhook→ 静态缓存(webhook:${method}-${path})→ 静态数据库记录 → 动态路径探测(路径中带:param段的情况;路径参数会被复制进request.params)。 -
加载已发布的版本。
loadWebhookExecutionData使用workflow.activeVersion(或者在某个开关后面的新发布服务)------nodes/connections来自已发布的版本,绝不是草稿。构建出一个Workflow;getBase(...)创建additionalData(凭据辅助对象、钩子占位、设置、生产调用下的userId= 发布者)。 -
表达式隔离实例------仅在需要时才创建。
webhookPhaseNeedsIsolate会跳过创建 V8 隔离实例,前提是满足一个很常见的场景:Webhook 节点 v2 及以上版本、参数是静态的、描述字段能原生解析。只要有任何一点无法证明是静态的,就会去获取一个隔离实例。 -
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()。这一步出错会被上报,并以一个通用错误作答;不会创建任何执行记录。
- 通过
-
由节点来决定。 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→ 不会创建执行)。 -
响应模式 (决定调用方在等什么):
模式 调用方会收到...... onReceived(默认)一旦执行记录存在,就立刻收到一个 JSON 回复:如果配置了 responseData就用它,否则是{ "message": "Workflow was started" }lastNode运行结束时最后一个执行节点的输出 responseNodeRespond to Webhook 节点发送的任何内容 streaming运行过程中的分块流( sendChunk钩子)formPage/hostedChatForm / Chat 触发器对应的界面页面流程 -
启动运行。
prepareExecutionData构建出初始的IRunExecutionData------关键是executionData.nodeExecutionStack = [{ node: <起始节点>, data: { main: <workflowData> }, source: null }]------然后:tsexecutionId = 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):
-
createWebhookHandlerFor拼出params.path=orders。调试日志:Received webhook "POST" for path "orders"。 -
findWebhook('POST','orders')→ 缓存未命中 → 数据库命中(orders, POST)→ 写入缓存。 -
加载已发布版本;
webhookPhaseNeedsIsolate返回假(Webhook v2、参数是静态的)→ 不构建隔离实例。 -
runWebhook调用Webhook.webhook(ctx);没有配置认证,validateAuth通过;结果:json{ "workflowData": [[ { "json": { "headers": { "content-type": "application/json", "...": "..." }, "params": {}, "query": {}, "body": { "customer": "ACME", "amount": 100 } } } ]] } -
prepareExecutionData把这个数据项放进初始栈里;WorkflowRunner.run被调用(S6 开始)。它返回例如"1042";直到这时接入层才发出:httpHTTP/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》,讲清楚一次执行是怎么被创建、又被决定跑在哪里的。
📚 返回专栏目录