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

Next.js中Server Sent Event连接无法关闭,Firestore读取请求异常求助

问题分析与解决方案

核心问题原因

  1. API路由错误终止SSE连接:你的API路由在设置Firestore快照监听后立即调用res.end(),直接关闭了HTTP连接,但此时req.on('close')事件还未触发,导致Firestore的onSnapshot监听没有被正确取消——服务器端会持续维持该监听,不断产生读取请求。
  2. EventSource自动重连的副作用:当SSE连接意外断开(比如切换标签页、网络波动),EventSource默认会自动重连,每次重连都会在服务器端创建新的Firestore快照监听,旧监听未清理就会累积,导致读取请求暴增。
  3. 未覆盖全场景连接清理:组件仅在encodedSlug变化时关闭旧连接,但没处理标签页切换、组件意外卸载等边缘场景,导致连接残留。

修复步骤

1. 修正API路由,正确维持SSE连接并清理Firestore监听

修改pages/api/messages.ts:

import { database } from '@/utils/firebase';
import type { NextApiRequest, NextApiResponse } from 'next';

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method !== 'GET') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  // 设置SSE必需响应头,防止代理/浏览器缓冲
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');

  const chatId = req.query.chatId as string;
  if (!chatId) {
    res.write(`data: ${JSON.stringify({ error: 'Chat ID is required' })}\n\n`);
    return res.end();
  }

  try {
    const chatRef = database.collection('chats').doc(chatId);
    const chatData = await chatRef.get();

    if (!chatData.exists) {
      res.write(`data: ${JSON.stringify({ error: 'Chat not found' })}\n\n`);
      return res.end();
    }

    // 发送初始数据并立即推送
    res.write(`data: ${JSON.stringify({ chats: chatData.data() })}\n\n`);
    await res.flush();

    // 监听Firestore快照更新
    const unsubscribe = chatRef.onSnapshot((doc) => {
      if (doc.exists) {
        const data = JSON.stringify({ chats: doc.data() });
        res.write(`data: ${data}\n\n`);
        res.flush();
      }
    }, (error) => {
      // 监听出错时推送错误并结束连接
      res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
      res.end();
    });

    // 客户端断开时清理Firestore监听
    const cleanup = () => {
      unsubscribe();
      res.end();
    };

    req.on('close', cleanup);
    req.on('error', cleanup);

  } catch (error) {
    console.error(error);
    res.write(`data: ${JSON.stringify({ error: 'Error retrieving chat' })}\n\n`);
    res.end();
  }
};

export default handler;

关键修改:

  • 移除错误的res.end()调用,仅在无数据、出错或客户端断开时结束连接
  • 添加res.flush()确保SSE数据实时推送
  • 同时监听req.close和req.error事件,保证Firestore监听被彻底清理
  • 设置完整的SSE响应头,避免连接异常

2. 优化组件的EventSource管理,覆盖全场景清理

修改Messages组件:

'use client';
import styles from './Messages.module.scss';
import React, { useState, useContext, useEffect, useRef } from 'react';
import Message from '../Message/Message';
import { ChatContext } from '@/context/ChatContext';
import { useMessages } from '@/hooks/useMessages';
import { MessageData } from '@/typedef';

export default function Messages() {
  const [messages, setMessages] = useState<Array<MessageData>>([]);
  const eventSourceRef = useRef<EventSource | null>(null);

  const { data: chatContextData } = useContext(ChatContext);
  const slug = chatContextData.chatId || '';
  const encodedSlug = encodeURIComponent(slug);
  const { isLoading } = useMessages(slug, Boolean(slug));

  const createEventSource = () => {
    if (!encodedSlug) return;
    
    // 先关闭旧连接
    if (eventSourceRef.current) {
      eventSourceRef.current.close();
    }

    const eventSource = new EventSource(`/api/messages?chatId=${encodedSlug}`);
    
    eventSource.onmessage = (event) => {
      const message = JSON.parse(event.data);
      if (message.error) {
        console.error(message.error);
        eventSource.close();
        return;
      }
      setMessages(message?.chats?.messages);
    };

    // 连接出错时主动关闭,避免自动重连
    eventSource.onerror = () => {
      eventSource.close();
      console.error('SSE connection error');
    };

    eventSourceRef.current = eventSource;
  };

  useEffect(() => {
    createEventSource();

    // 标签页隐藏时关闭连接,显示时重建
    const handleVisibilityChange = () => {
      if (document.hidden) {
        eventSourceRef.current?.close();
        eventSourceRef.current = null;
      } else if (slug) {
        createEventSource();
      }
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);

    // 组件卸载时彻底清理
    return () => {
      eventSourceRef.current?.close();
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, [encodedSlug, slug]);

  return (
    <div className={styles.messages}>
      {isLoading && <p>Loading...</p>}
      {messages?.map((message: MessageData) => (
        <Message message={message} key={message.id} />
      ))}
    </div>
  );
}

关键优化:

  • 用useRef存储EventSource实例,方便跨渲染周期访问和清理
  • 添加onerror事件处理,避免连接出错后自动重连
  • 监听visibilitychange事件,减少后台不必要的连接和Firestore读取
  • 组件卸载时彻底清理连接和事件监听

3. 可选:优化SWR Hook(移除或协同工作)

当前SWR会发起普通HTTP请求,而SSE已经实时推送数据,可直接移除useMessages调用,完全依赖SSE获取初始和更新数据;若要保留SWR,可让它负责初始数据加载,SSE负责后续更新。

为什么Firestore读取持续增加?

API路由中res.end()提前关闭了HTTP连接,但Firestore的onSnapshot监听未被取消——服务器端Node.js进程会继续维持该监听,每次数据变化都会触发读取。同时EventSource的自动重连会不断创建新的API请求,每个请求都会生成新的Firestore监听,最终导致读取请求指数级增长。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 04:55:23