最近业务部门找到我们,说一个对接 Dify chatflow 流式接口的功能"炸了":六月份之前一直能正常解析的返回结果,现在直接解析失败。之前的对接代码是在第一条报文的data里等workflow_started事件来判断"任务已受理"的。现在第一条报文变成了一行光秃秃的event: ping,连data:` 都没有,解析逻辑直接傻眼。
排查:先入为主,把矛头指向了业务部门
接到反馈后,我们一开始根本没打算抓包。原因很简单——想当然:一个 HTTP 接口,怎么可能返回 event: ping 这种离谱的东西?这玩意儿连 JSON 都不是。第一反应是业务部门搞错了。
于是我们翻了网络日志,又翻了 Dify 后台日志。网络日志里 HTTP 状态码清一色 200,请求一切正常;Dify 后台里用户发送的问题、完整的回答结果都清清楚楚躺在那里,说明调用链路和业务处理都没毛病。这更加坚定了我们的判断:就是业务部门在乱打日志,不知道把什么别的东西当成了接口返回。
但业务部门一口咬定:收到的原始报文里,第一条就是一行 event: ping。两边各执一词,僵住了。
没办法,只能真去抓包看原始报文。结果看到的一刻我们傻眼了——它真的在:
event: ping
data: {"event":"workflow_started","workflow_run_id":"...","data":{"id":"...","workflow_id":"...","sequence_number":0}}
data: {"event":"node_started","workflow_run_id":"...","data":{"node_type":"start",...}}

event: ping 真实存在,是我方接口发出去的。之前怪业务部门乱打日志,是我们错怪了人家。接下来才是各种离谱猜测轮番上阵:
- 怀疑是不是服务器上的 ping 测试被"透传"回了接口。 我们确实在服务器上执行过
ping命令做连通性测试——会不会被什么中间层透传进了响应?虽然稍微懂点技术就知道这完全不可能,但当时真的什么想法都冒得出来。 - 怀疑网关、代理、负载均衡在注入心跳。 健康检查、长连接保活什么的,查了一圈配置,全都对不上。
- 最后查了Dify的工作流编排。 工作流编排从上线起没动过,也不可能主动输出这种格式。
排查彻底陷入僵局。我们把希望寄托在 Dify 官方文档上,结果文档也没能救我们。
文档确实把各种 event 类型列得清清楚楚:message、message_end、workflow_started、node_started……在列表最底部,也确实提到了 event: ping——"每 10s 一次的 ping 事件,保持连接存活"。

但有两个致命的地方对不上:
- 文档开篇就说"每个流式块均为
data:开头",可我们看到的 ping 是event:开头、没有data:,和文档描述直接矛盾; - 文档又说 ping 是"每 10s 一次",可我们是第一包就收到了。
文档只罗列了 event 类型,从头到尾没有解释 ping 的特殊性——它是唯一不以 data: 开头的块。而且我们当时压根不知道 SSE 协议这回事,连"event: 开头的行也是一种合法响应格式"这个理论认知前提都不具备。文档翻烂了,也拼不出答案。
问官方:"一直有这个机制"
最后没办法,只能去问 Dify 官方支持人员。直到这时,我们才从他口中了解到 SSE 协议:
这个接口的 Content-Type 是 text/event-stream,走的是 Server-Sent Events,不是普通 HTTP 返回多个 JSON 消息。SSE 里每条事件由 event:(事件类型)、data:(载荷)等字段组成,以空行分隔。一条只有 event: 没有 data: 的报文在 SSE 里是完全合法的——它就是一个"带类型的空事件",最常见的用途就是心跳。而我们之前一直把Dify的流式接口当成一个会返回多个JSON消息的HTTP接口用,根本没仔细研究过SSE协议。
所以 event: ping 不是什么异常,它就是 Dify 在流式连接上发的一个心跳包。官方说:一直有 event: ping 这个机制。
这就矛盾了:如果一直有,为什么我们之前从来没收到过?为什么现在它变成了第一条报文?
这时我才开始把注意力放回环境本身,核对了对接以来用过的版本:
- 六月之前:企业版 3.8.0(对应社区版 1.12.1-hotfix.6),流式返回一直解析正常;
- 现在:企业版 3.9.6(对应社区版 1.13.3),
event: ping出现在首包。
结合官方"一直有这个机制"的说法,我怀疑问题就出在版本变化上。翻了两个版本的源码,果然——Dify 流式通道在 1.13.0 有过一次重构,ping 的发送时机彻底变了。
机制对比:ping 的位置变了
1.13.0 之前(1.12.x):ping 只是"兜底心跳"
旧版走的是内存队列 + 进程内消费的模式。工作流执行时,worker 把 workflow_started、node_started 这些事件写进内存队列,API 进程从队列里逐个取出来转成 SSE 返回。
首包就是 worker 发布的第一个事件——workflow_started。而 ping 只是一个兜底:只有当队列超过 10 秒没有新事件时,才会插入一个 ping 保活,防止连接超时。正常情况下根本轮不到它出场。
所以我们在 3.8.0 上一直没收到 ping,不是巧合,是旧机制里 ping 本来就很少出现。
1.13.0 之后(1.13.3):ping 变成"开场白"
2026 年 2 月,Dify 合入了 Human Input Node(人工输入节点)功能(PR #32060),顺带重构了流式通道。新机制改成了 Redis Pub/Sub 主题订阅:API 进程订阅一个 Redis 主题,worker 把事件广播到主题上,API 订阅到之后转发给客户端。
关键改动在订阅逻辑的开头,源码里是这么写的:
# send a PING event immediately to prevent the connection staying in pending state for a long time.
#
# This simplify the debugging process as the DevTools in Chrome does not
# provide complete curl command for pending connections.
yield StreamEvent.PING.value
一进入函数,就无条件先发一个 ping。注释说得很清楚:让连接立即有响应,不要长时间处于 pending 状态——因为 Chrome DevTools 对 pending 状态的请求不提供完整的 curl 命令,调试起来很痛苦。
于是从 1.13.0 开始,所有走新通道的流式响应,第一个包固定是 event: ping,第二个包才是 workflow_started。
这就解释了一切:不是"突然多了个 ping",而是 ping 从"10 秒空闲才会出现的兜底心跳"变成了"每次连接都先发的前置心跳"。我们在 3.8.0 上根本没机会见到它,升级到 3.9.6 之后它成了见面礼。
顺带一提,官方文档里"ping 每 10s 一次"的描述,描述的还是旧机制的兜底心跳;新机制下它是开场白,文档并没有同步更新——这又给我们的排查多添了一层堵。
对接怎么改
对客户端来说,改动其实很小:
1. 忽略没有 data: 的 ping 包
解析 SSE 时,遇到只有 event: ping、没有 data: 字段的报文,直接跳过。它既不是业务事件,也不代表出错,就是个心跳。
2. 判断"任务已受理"改用第一条 data: 事件
之前用"第一条报文"判断任务已受理,现在第一条可能是 ping。改成以第一个带 data: 的事件(即 workflow_started)为准。
3. 其余解析逻辑完全不动
workflow_started、node_started、message、workflow_finished 这些事件的结构和顺序都没有变,只多了一条需要跳过的开头心跳。
一些体会
这次排查真正卡住我们的,不是技术问题,而是认知盲区,而且这个盲区有两层。
第一层,我们压根不知道 SSE 协议,一直把流式接口当成"会返回一堆 JSON 的普通 HTTP 接口"在用。而 Dify 官方文档也"功不可没":它开篇就说"每个流式块均为 data: 开头",把 ping 埋在事件列表最底部一句带过,还写着"每 10s 一次"——偏偏新版本里它是首包、还不带 data:。文档没有告诉我们 ping 的特殊性,我们也不知道问什么。最后是从官方支持人员的口中,才补上了协议知识这块拼图。
第二层,也是最打脸的:我们最先排除的假设,恰恰是真相。接到反馈时我们想当然地认为"HTTP 不可能返回这种离谱的东西",连原始报文都没看就把锅扣给了业务部门。而且当时查的日志本身就很有迷惑性——网络日志全是 200,Dify 后台有完整问答记录,但这些只能证明"请求成功了、业务跑通了",根本看不到 SSE 的字节流长什么样。HTTP 200 和完整回答,与 event: ping 的存在完全不矛盾。如果一开始就抓包看原始响应,问题半天就能定位,根本不用绕这么大一圈。先入为主的判断,比技术盲区更致命。
另外,Dify 官方说"一直有这个机制"其实也没说错——ping 一直都在,变的只是它出现的时机。对接第三方平台时,协议里的"可选/兜底"行为,往往比"主流程"更容易在升级中改变,而且这种改变不会写进 changelog 的显著位置。升级依赖前,除了看新特性,还值得把流式、回调这类长连接协议的整体行为重新过一遍,最好用原始报文验证一次首包和尾包,而不是只信"数据结构没变"。
附:关键信息速查
| 项目 | 说明 |
|---|---|
| 现象 | chatflow 流式接口首包从workflow_started 变为 event: ping |
| 影响版本 | 社区版 1.13.0+(对应企业版 3.9.x);旧版 1.12.x 及以下不受影响 |
| 变更来源 | 上游 PR #32060(Human Input Node,2026-02-09)重构流式通道 |
| 旧机制 | 内存队列消费,ping 仅作为空闲 10 秒兜底心跳 |
| 新机制 | Redis Pub/Sub 订阅,订阅前无条件先发一个 ping |
| 客户端改法 | 跳过无data: 的 ping 包,以第一条 data: 事件(workflow_started)作为任务受理标志 |
标题:踩坑记录:Dify 1.13 升级后,流式接口首包不再是 data: {"event": "workflow_started"} 而变成了 event: ping
作者:aopstudio
地址:https://neusoftware.top/articles/2026/08/04/1785813050163.html