Skip to content

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-alive

4. 测试连接

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. 连接超时

现象:连接建立后很快断开

原因

  1. Nginx 超时配置
  2. 后端超时配置
  3. 没有心跳机制

解决

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-polyfill
typescript
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_401Token 无效刷新 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
  }
})

相关链接

基于 MIT 许可发布 · 由 ❤️ 和 ☕ 驱动 · 支持作者