SSE 连接排查
问题表现
前端 SSE 连接失败或断开:
EventSource's response has a MIME type that is not text/event-stream或
SSE connection error排查步骤
1. 检查环境变量
bash
# .env
VITE_APP_SSE_ENDPOINT=http://localhost:8080/api/v1/sse/connect确认:
- 地址是否正确
- 协议是否匹配(http/https)
- 端口是否正确
2. 检查连接状态
typescript
import { useSse } from '@/composables'
const { isConnected, reconnectAttempts } = useSse()
console.log('连接状态:', isConnected.value)
console.log('重连次数:', reconnectAttempts.value)3. 浏览器 Network 标签
查看请求:
Name: connect
Status: 200
Type: eventsource检查响应头:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive4. 测试连接
typescript
// 临时测试
const testSse = () => {
const token = localStorage.getItem('accessToken')
const url = `${import.meta.env.VITE_APP_SSE_ENDPOINT}?accessToken=${token}`
const eventSource = new EventSource(url)
eventSource.onopen = () => {
console.log('✅ SSE 连接成功')
}
eventSource.onerror = (error) => {
console.error('❌ SSE 连接失败:', error)
}
eventSource.onmessage = (event) => {
console.log('📨 收到消息:', event.data)
}
}常见问题
1. MIME 类型错误
错误:
EventSource's response has a MIME type that is not text/event-stream原因:后端返回的 Content-Type 不正确
解决:
java
@GetMapping("/sse/connect")
public void connect(HttpServletResponse response) {
response.setContentType("text/event-stream")
response.setCharacterEncoding("UTF-8")
response.setHeader("Cache-Control", "no-cache")
response.setHeader("Connection", "keep-alive")
// ...
}2. 跨域问题
错误:
Access to EventSource at 'http://localhost:8080/api/v1/sse/connect'
from origin 'http://localhost:5173' has been blocked by CORS policy原因:SSE 不支持自定义请求头
解决:
方案 1:Token 放到 URL 参数
typescript
const url = `/api/v1/sse/connect?accessToken=${token}`
const eventSource = new EventSource(url)方案 2:后端配置 CORS
java
response.setHeader("Access-Control-Allow-Origin", "*")3. 连接超时
现象:连接建立后很快断开
原因:
- Nginx 超时配置
- 后端超时配置
- 没有心跳机制
解决:
Nginx 配置:
nginx
location /api/v1/sse/connect {
proxy_pass http://backend:8080;
proxy_http_version 1.1;
proxy_set_header Connection '';
# 禁用缓冲
proxy_buffering off;
# 超时设置
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}后端心跳:
java
// 每 30 秒发送心跳
while (true) {
Thread.sleep(30000)
emitter.send(SseEmitter.event()
.name("heartbeat")
.data("{\"time\":" + System.currentTimeMillis() + "}"))
}4. Token 过期
现象:连接建立后立即断开
原因:accessToken 过期
解决:
typescript
// 检查 Token 是否有效
const token = localStorage.getItem('accessToken')
const decoded = jwtDecode(token)
const isExpired = decoded.exp * 1000 < Date.now()
if (isExpired) {
// 刷新 Token
await refreshToken()
}5. 浏览器兼容性
问题:IE 不支持 EventSource
解决:使用 polyfill
bash
pnpm add event-source-polyfilltypescript
import { EventSourcePolyfill } from 'event-source-polyfill'
const eventSource = new EventSourcePolyfill(url, {
headers: {
'Authorization': `Bearer ${token}`
}
})调试工具
1. 在线测试
typescript
// 在浏览器 Console 中测试
const testSse = () => {
const es = new EventSource('/api/v1/sse/connect?accessToken=YOUR_TOKEN')
es.onmessage = e => console.log(e.data)
es.onerror = e => console.error(e)
}2. curl 测试
bash
curl -N -H "Authorization: Bearer YOUR_TOKEN" \
http://localhost:8080/api/v1/sse/connect预期输出:
event: heartbeat
data: {"time":1705312800000}
event: online_count
data: {"count":42}3. 后端日志
java
@Slf4j
@RestController
public class SseController {
@GetMapping("/sse/connect")
public SseEmitter connect() {
log.info("SSE 连接建立")
SseEmitter emitter = new SseEmitter(0L)
emitter.onCompletion(() -> log.info("SSE 连接关闭"))
emitter.onTimeout(() -> log.warn("SSE 连接超时"))
emitter.onError(e -> log.error("SSE 连接错误", e))
return emitter
}
}最佳实践
1. 自动重连
typescript
const { connect, disconnect } = useSse()
// 组件挂载时连接
onMounted(() => {
connect()
})
// 组件卸载时断开
onUnmounted(() => {
disconnect()
})2. 指数退避
typescript
const reconnect = (attempt: number) => {
const delay = Math.min(1000 * Math.pow(2, attempt), 30000)
setTimeout(() => {
connect()
}, delay)
}3. 心跳检测
typescript
let lastHeartbeat = Date.now()
const timeout = 60000 // 60 秒无心跳则重连
setInterval(() => {
if (Date.now() - lastHeartbeat > timeout) {
console.warn('心跳超时,重新连接')
disconnect()
connect()
}
}, 10000)常见错误码
| 错误 | 原因 | 解决 |
|---|---|---|
NETWORK_ERROR | 网络断开 | 检查网络连接 |
TIMEOUT | 连接超时 | 增加超时时间 |
HTTP_401 | Token 无效 | 刷新 Token |
HTTP_403 | 无权限 | 检查用户权限 |
HTTP_404 | 接口不存在 | 检查 URL |
HTTP_500 | 服务器错误 | 查看后端日志 |
性能优化
1. 避免重复连接
typescript
if (isConnected.value) {
console.warn('SSE 已连接,跳过')
return
}2. 控制消息频率
typescript
// 使用 throttle 限制消息处理频率
const handleMessage = throttle((data) => {
// 处理消息
}, 1000)3. 合理的缓存
typescript
// 缓存最新状态,避免重复渲染
const lastOnlineCount = ref(0)
on('online_count', (data) => {
if (data.count !== lastOnlineCount.value) {
lastOnlineCount.value = data.count
// 更新 UI
}
})