Next.js中Server Sent Event连接无法关闭,Firestore读取请求异常求助
问题分析与解决方案
核心问题原因
- API路由错误终止SSE连接:你的API路由在设置Firestore快照监听后立即调用
res.end(),直接关闭了HTTP连接,但此时req.on('close')事件还未触发,导致Firestore的onSnapshot监听没有被正确取消——服务器端会持续维持该监听,不断产生读取请求。 - EventSource自动重连的副作用:当SSE连接意外断开(比如切换标签页、网络波动),EventSource默认会自动重连,每次重连都会在服务器端创建新的Firestore快照监听,旧监听未清理就会累积,导致读取请求暴增。
- 未覆盖全场景连接清理:组件仅在
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
相关产品推荐
相关产品推荐

