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

刷新页面未断开Solidjs与Golang后端的SSE连接问题求助

问题分析与解决方案

问题现象

开发的SSE API首次运行正常,但页面刷新后,需多次调用POST接口才能触发前端接收SSE推送;发送次数过多会出现“连接已关闭”错误,之后恢复正常,直到再次刷新页面/关闭标签页。调试发现服务器端ctx.Done()逻辑未触发,推测页面刷新时客户端或服务器未正确关闭SSE连接,导致旧连接残留阻塞消息推送。


前端代码问题排查与修复

当前代码存在以下问题:

  1. createEffect每次触发时直接创建新EventSource,未先关闭旧连接,导致多连接残留
  2. onCleanup可能因页面刷新时机问题未及时执行,无法保证连接关闭
  3. EventSource的错误处理逻辑不够严谨

修复后的前端代码

const [eventSource, setEventSource] = createSignal(undefined);

// 监听页面卸载事件,主动关闭连接
useEffect(() => {
  const handleUnload = () => {
    eventSource()?.close();
  };
  window.addEventListener('beforeunload', handleUnload);
  return () => {
    window.removeEventListener('beforeunload', handleUnload);
  };
}, []);

createEffect(() => {
  // 先关闭旧连接再创建新连接
  const oldSource = eventSource();
  if (oldSource) {
    oldSource.close();
    console.log("Closed old connection");
  }

  const newSource = new EventSource(url);
  setEventSource(newSource);
  console.log("Connected to SSE");

  newSource.onmessage = (event) => {
    // 处理接收的数据
  };

  newSource.onerror = (event) => {
    console.error(`SSE Connection Error: ${event}`);
    newSource.close();
    setEventSource(undefined);
  };

  newSource.onopen = () => {
    console.log("SSE Connection Established");
  };
});

onCleanup(() => {
  console.log("Cleanup: Closing SSE Connection");
  eventSource()?.close();
});

前端关键修复点

  • 创建新连接前关闭旧连接:避免同一页面存在多个SSE连接,导致消息被分发到已失效的旧连接
  • 监听beforeunload事件:在页面刷新、关闭或跳转时主动触发连接关闭,确保客户端及时通知服务器
  • 严谨的错误处理:连接出错时立即关闭并清空引用,防止无效连接残留

后端代码问题排查与修复

当前后端未设置连接存活检测,导致客户端断开后服务器无法及时感知,旧连接持续占用dataChannel,新消息被阻塞在无效连接中。

修复后的后端代码

func sseFunction(appCtx *fiber.Ctx, dataChannel chan map[string]any) error {
    setHeader(appCtx)
    // 设置TCP KeepAlive,帮助服务器检测死连接
    appCtx.Set(fiber.HeaderConnection, "keep-alive")
    appCtx.Set(fiber.HeaderKeepAlive, "timeout=5, max=100")

    ctx := appCtx.Context()
    // 创建心跳定时器,每30秒发送一次空心跳
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()

    ctx.SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
        log.Println("SSE Connection Opened")
        defer log.Println("SSE Connection Closed")

        for {
            select {
            case message := <-dataChannel:
                jsonString, err := json.Marshal(message)
                if err != nil {
                    log.Printf("Marshal error: %v", err)
                    continue
                }
                if _, err := fmt.Fprintf(w, "data: %s\n\n", jsonString); err != nil {
                    log.Printf("Write message error: %v", err)
                    return
                }
                if err := w.Flush(); err != nil {
                    log.Printf("Flush error, closing connection: %v", err)
                    return
                }
            case <-ticker.C:
                // 发送心跳消息,维持连接并检测存活
                if _, err := fmt.Fprintf(w, "data: \n\n"); err != nil {
                    log.Printf("Heartbeat write error: %v", err)
                    return
                }
                if err := w.Flush(); err != nil {
                    log.Printf("Heartbeat flush error, closing connection: %v", err)
                    return
                }
            case <-ctx.Done():
                log.Println("Client closed connection via ctx.Done()")
                return
            }
        }
    }))

    return nil
}

后端关键修复点

  • 启用TCP KeepAlive:通过响应头设置连接存活参数,让TCP层主动检测死连接
  • 添加SSE心跳机制:定期发送空消息,既能维持连接,又能在客户端断开时通过Flush()错误及时感知并关闭连接
  • 增强错误处理:对消息写入、刷新操作的错误直接返回,确保无效连接被及时清理

核心原理说明

页面刷新时,浏览器会断开旧的HTTP连接,但TCP连接可能存在延迟关闭的情况。通过前端主动调用EventSource.close()和后端心跳检测,能确保旧连接被及时识别并清理,避免dataChannel的消息被分发到无效连接,从而解决多次POST才触发推送的问题。

内容的提问来源于stack exchange,提问作者danialsaufi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 16:34:53