You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

React客户端在Node.js从循环式SSE流切换为POST触发式后无法接收事件的原因排查

React客户端在Node.js从循环式SSE流切换为POST触发式后无法接收事件的原因排查

我之前用Hono实现触发式SSE推送时也遇到过几乎一模一样的问题——服务器日志明明显示调用了writeSSE,但客户端完全没反应。后来排查下来,核心问题集中在SSE长连接的保持流对象的生命周期上,咱们一步步拆解:

一、核心问题分析

1. SSE连接因空闲被自动关闭

循环版本的SSE能正常工作,是因为while(true)循环持续调用writeSSEstream.sleep(1000),一直让HTTP连接处于活跃状态,服务器和中间件不会主动断开它。

但你的POST触发版本中,GET /sse接口的流回调只做了三件事:

  • 把stream加入活跃集合
  • 发送初始消息
  • 注册onAbort清理函数

之后回调函数就执行完毕了,没有任何持续的异步操作来“维持”连接。此时Node.js的HTTP服务器或Hono的默认配置会判定这个连接为空闲状态,过段时间就会自动关闭。客户端的EventSource表面上没触发error,但实际上已经和服务器断开了,这时调用writeSSE只是在操作一个失效的流对象而已。

2. Hono streamSSE的生命周期限制

Hono的streamSSE基于异步迭代器实现,如果回调函数没有持续的异步任务(比如循环sleep、等待外部信号),Hono会认为响应已处理完成,主动结束HTTP响应并关闭SSE连接。这时候哪怕你把stream存在activeStreams里,这个对象也已经失去了和客户端通信的能力。

3. 无效流对象未被清理

你已经在onAbort里做了流的清理,但如果连接是被服务器超时关闭(而非客户端主动断开),onAbort可能不会触发,失效的stream会一直留在activeStreams里。POST触发时调用这些无效流的writeSSE不会抛出明显错误,但数据根本发不到客户端。

二、修复方案和代码调整

1. 保持SSE连接活跃

GET /sse的流回调中添加无限循环的sleep操作,模拟心跳来防止连接因空闲超时关闭。同时可以额外发送心跳事件,让客户端感知连接状态:

// api.ts
const activeStreams: Set<any> = new Set();
let id = 0;

// 把POST触发路由改成独立路径,避免和GET /sse混淆
app.post("/sse-trigger", async (c) => {
  const message = `It is ${new Date().toISOString()}`;
  const invalidStreams = new Set();

  for (const stream of activeStreams) {
    try {
      await stream.writeSSE({
        data: message,
        event: "time-update",
        id: String(id++),
      });
      console.log("time-update sent successfully:", message);
    } catch (err) {
      console.error("Failed to send to stream:", err);
      invalidStreams.add(stream);
    }
  }

  // 及时清理无效流,避免集合膨胀
  for (const stream of invalidStreams) {
    activeStreams.delete(stream);
  }

  return c.json({ status: "ok", sentTo: activeStreams.size });
});

app.get("/sse", async (c) => {
  return streamSSE(c, async (stream) => {
    activeStreams.add(stream);

    // 连接断开时主动清理流
    stream.onAbort(() => {
      activeStreams.delete(stream);
      console.log("Stream disconnected, removed from active list");
    });

    // 发送初始连接确认
    await stream.writeSSE({
      data: `Connected at ${new Date().toISOString()}`,
      event: "time-update",
      id: String(id++),
    });

    // 无限循环sleep保持连接活跃,每30秒发一次心跳
    while (true) {
      await stream.sleep(30000);
      // 可选:发送空心跳事件,让客户端感知连接状态
      await stream.writeSSE({ event: "heartbeat", data: "" });
    }
  });
});

2. 客户端添加连接状态验证

在React客户端的EventSource中添加onopenonclose监听,直观确认连接是否真的保持:

// app.tsx
function Component() {
  const [time, setTime] = useState<string[]>([]);

  useEffect(() => {
    const eventSource = new EventSource("http://localhost:8787/sse");

    eventSource.onopen = () => {
      console.log("✅ EventSource connection established");
    };

    eventSource.onclose = () => {
      console.log("❌ EventSource connection closed");
    };

    eventSource.addEventListener("time-update", (event) => {
      setTime((prev) => [event.data, ...prev]);
    });

    // 心跳事件可忽略,仅用于验证连接
    eventSource.addEventListener("heartbeat", () => {
      console.log("💓 Received heartbeat from server");
    });

    eventSource.onerror = (err) => {
      console.error("EventSource error:", err);
      eventSource.close();
    };

    return () => {
      eventSource.close();
    };
  }, []);

  return (
    <div className="mb-150">
      <h1>Server Time</h1>
      <ul className="flex flex-col">
        {time.map((t, index) => (
          <li key={index}>{t}</li>
        ))}
      </ul>
    </div>
  );
}

3. 额外注意事项

  • 避免同路径的GET/POST:虽然Hono支持同路径不同HTTP方法,但把触发路由改成/sse-trigger这类独立路径,能减少调试时的混淆。
  • 捕获写入错误:POST触发时一定要捕获writeSSE的异常,及时清理无效流,避免集合里积累大量失效对象。
  • 调整服务器超时:如果你的Node.js服务器有全局HTTP超时设置(比如server.timeout),可以适当调大(生产环境不建议禁用),确保长连接不会被强制关闭。

三、验证方法

  1. 启动服务器和客户端,打开浏览器控制台,确认能看到✅ EventSource connection established的日志。
  2. 用Postman或curl调用POST /sse-trigger接口,看客户端是否能收到新的时间消息。
  3. 等待30秒,看客户端是否收到💓 Received heartbeat from server的日志,确认连接保持正常。

内容来源于stack exchange

火山引擎 最新活动