React18.2+StompJS6升级后WebSocket异常,如何过滤ECONNRESET日志
问题
首次开发React项目,将基于React 16+@stomp/stompjs 5.4.4的Demo页面迁移至React 18.2+StompJS 6.1.2版本后,WebSocket连接出现异常:当后端服务未启动时,新版本在多次重试后会触发WebSocket错误,终端输出如下内容:
[HPM] WebSocket error: Error: read ECONNRESET at TCP.onStreamRead (node:internal/stream_base_commons:217:20) { errno: -4077, code: 'ECONNRESET', syscall: 'read' }
而旧版本仅会显示ECONNREFUSED类代理错误。
尝试通过配置http-proxy-middleware的日志回调过滤该ECONNRESET错误信息,但配置后无日志输出,现咨询如何正确配置日志以过滤该错误日志。
新旧版本代码对比
旧版本代码
package.json
"dependencies": { "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@stomp/stompjs": "^5.4.4", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.2", "@testing-library/user-event": "^12.1.3", "clsx": "^1.1.1", "prop-types": "^15.7.2", "react": "^16.13.1", "react-dom": "^16.13.1", "react-redux": "^7.2.1", "react-router-dom": "^5.2.0", "react-scripts": "^3.4.3", "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8", "sockjs-client": "^1.5.0", "typeface-roboto": "0.0.75", "typescript": "^4.0.2" },
setupProxy.js
const proxy = require("http-proxy-middleware"); module.exports = function(app) { app.use(proxy("/ws", {target: "http://localhost:8080", ws: true})); }
WsConstants.js(调试环境连接路径为/ws)
import SockJS from "sockjs-client"; let backendApiUrl; // 连接Socket的地址 if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { // 开发环境 backendApiUrl = '/ws'; } else { // 生产环境 http://localost:8080/ws backendApiUrl = window['runConfig'].backendApiUrl; } export const SOCKET = () => new SockJS(backendApiUrl);
App.js
import React from 'react'; import {BrowserRouter as Router, Route, Switch} from "react-router-dom"; import Mcd from "../mcd/Mcd"; import StitchingTool from "../stitching/StitchingTools"; import BoundingTool from "../bounding/BoundingTools"; import PtzToPanoTool from "../ptztopano/PtzToPanoTools"; import LocationTool from "../location/LocationTools"; import {SOCKET} from "../../models/app/WsConstants"; import {Stomp} from "@stomp/stompjs/esm6"; import * as actionTypes from "./store/actionTypes"; import {connect} from "react-redux"; import Layout from "./Layout"; class App extends React.Component { constructor(props) { super(props); this.state = { stompClient: Stomp.over(SOCKET), cameras: {} }; // 注释掉此行可启用Stomp日志 this.state.stompClient.debug = ()=>{}; this.state.stompClient.reconnectDelay = 3000; this.state.stompClient.onConnect = () => this.handleWsConnect(this.state.stompClient); this.state.stompClient.onDisconnect = this.handleWsDisconnect; this.state.stompClient.onStompError = this.handleWsDisconnect; this.state.stompClient.onWebSocketClose = this.handleWsDisconnect; this.state.stompClient.onWebSocketError = this.handleWsDisconnect; this.state.stompClient.activate(); } handleWsConnect = (stompClient) => { console.log(' ------------ WS CONNECTED! ------------ '); stompClient.subscribe("/mcd/cameras", (res) => { this.setState({...this.state, cameras: JSON.parse(res.body || '{}')}); }); stompClient.send("/mcd/cameras"); this.props.onWsConnected(); } handleWsDisconnect = (event) => { console.error(' ------------ WS DISCONNECTED! ------------\n' + JSON.stringify(event)); this.props.onWsDisconnected(); } componentWillUnmount() { if (!this.state.stompClient) { this.state.stompClient.deactivate(); } } render() { return ( <Layout wsConnected={this.props.app.wsConnected}> <Router> <Switch> <Route exact path="/" render={() => <Mcd stompClient={this.state.stompClient}/>}/> <Route path="/stitchingtool" render={() => <StitchingTool cameras={this.state.cameras} stompClient={this.state.stompClient}/>}/> <Route path="/boundingtool" render={() => <BoundingTool stompClient={this.state.stompClient}/>}/> <Route path="/ptztopanotool" render={() => <PtzToPanoTool stompClient={this.state.stompClient}/>}/> {/*<Route path="/overlaytool" render={() => <OverlayTool stompClient={this.state.stompClient}/>}/>*/} <Route path="/locationtool" render={() => <LocationTool stompClient={this.state.stompClient}/>}/> </Switch> </Router> </Layout> ); } }
新版本代码
package.json
"dependencies": { "@emotion/react": "^11.10.4", "@emotion/styled": "^11.10.4", "@fontsource/roboto": "^4.5.8", "@mui/icons-material": "^5.10.9", "@mui/material": "^5.10.10", "@stomp/stompjs": "^6.1.2", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "http-proxy-middleware": "^2.0.6", "lodash.clonedeep": "^4.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", "sockjs-client": "^1.6.1", "web-vitals": "^2.1.4" }, "devDependencies": { "react-router-dom": "^6.4.2", "typescript": "^4.8.4" }
setupProxy.js
const { createProxyMiddleware } = require("http-proxy-middleware"); module.exports = function(app) { app.use(createProxyMiddleware("/ws", {target: "http://localhost:8080", ws: true})); }
WsConstants.js(无修改)
import SockJS from "sockjs-client"; let backendApiUrl; // 连接Socket的地址 if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { // 开发环境 backendApiUrl = '/ws'; } else { // 生产环境 http://localost:8080/ws backendApiUrl = window['runConfig'].backendApiUrl; } export const SOCKET = () => new SockJS(backendApiUrl);
App.js
/* 该组件用于在页面顶部创建导航栏,选中的功能将加载到页面下半部分。 */ import React, { useEffect, useState, useRef } from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import {SOCKET} from "../../models/app/WsConstants"; import {Stomp} from "@stomp/stompjs/esm6"; import Mcd from "../mcd/Mcd"; import LocationTool from "../location/LocationTool"; import NoPage from "../nav/NoPage"; import {Navbar, NavLayout} from "../nav/Navbar"; /* 这是页面的主入口点,分为两行,顶部是导航栏。 Mcd是默认页面,添加新页面需新增Route块。 */ function App() { const [wsConnected, setWsConnected] = useState(false); const wsSetupComplete = useRef(false); var stompClient = useRef(Stomp.over(SOCKET)); /* 设置WebSocket连接相关配置,仅在页面加载时执行一次。 */ const connectAndSetupOnlyOnce = () => { // 是否已完成Socket配置? if (wsSetupComplete.current) { return; } // 注释掉此行可启用Stomp日志 stompClient.current.debug = () => {}; // 启用Stomp日志 //stompClient.current.debug = function(str) { // console.log(str); // } // 自动重连延迟(毫秒),设为0则禁用,此处为3秒延迟 stompClient.current.reconnectDelay = 3000; // 成功连接到STOMP broker时的回调 stompClient.current.onConnect = handleWsConnect; // 成功断开与STOMP broker连接时的回调(因错误断开时不会触发) stompClient.current.onDisconnect = handleWsDisconnect; // 收到STOMP Broker发送的ERROR帧时的回调,合规Broker发送后会关闭连接 stompClient.current.onStompError = handleWsErrors; // 底层WebSocket关闭时的回调 stompClient.current.onWebSocketClose = handleWsDisconnect; // 底层WebSocket触发错误时的回调 stompClient.current.onWebSocketError = handleWsErrors; // 未处理的消息回调 stompClient.current.onUnhandledMessage = unhandleWsErrors; // 未处理的回执回调 stompClient.current.onUnhandledReceipt = unhandleWsErrors; // 未处理的关闭回调 stompClient.current.onUnhandledClose = unhandleWsErrors; // 未处理的帧回调 stompClient.current.onUnhandledFrame = unhandleWsErrors; // 初始化与broker的连接,连接断开后会按reconnectDelay设置自动重连 stompClient.current.activate(); wsSetupComplete.current = true; // Websocket配置完成 } /* 仅在页面重绘时调用。 注意:主页面新增内容会触发重绘,导致useEffect重复执行Socket配置逻辑,因此将逻辑移至仅执行一次的函数中。 注释eslint-disable-next-line用于抑制connectAndSetupOnlyOnce不是useEffect依赖的警告,此处作为App.js的初始化逻辑无需添加依赖。 */ useEffect(() => { connectAndSetupOnlyOnce(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); /* 连接成功时的回调,设置wsConnected为true。 */ function handleWsConnect() { console.log(' ------------ WS CONNECTED! ------------ '); setWsConnected(true); } /* 断开连接时的回调,更新wsConnected状态为false。 @param event 事件对象 */ function handleWsDisconnect(event) { console.error(' ------------ WS DISCONNECTED! ------------ '); if (typeof(event) != "undefined") { let newDate = new Date(); newDate.setTime(event.timeStamp); console.log('WebSocket断开时间 : ' + newDate.toUTCString() + " 原因 : " + event.reason); } setWsConnected(false); } /* 错误处理回调,记录错误并更新wsConnected状态为false。 @param frame 事件对象 注意: 合规的broker会在`message`头中携带简短消息,body可能包含详细信息。 合规的broker在发送错误后会终止连接 */ function handleWsErrors(frame) { console.error(' ------------ WS Error! ----------- '); setWsConnected(false); if (typeof(frame) != "undefined") { console.log('Broker报告错误: ' + frame.headers['message']); console.log('详细信息: ' + frame.body); } } /* 未处理错误的回调,记录错误并更新wsConnected状态为false。 @param frame 事件对象 */ function unhandleWsErrors(frame) { console.error(' ------------ 未处理的WS错误! ----------- '); setWsConnected(false); if (typeof(frame) != "undefined") { console.log('Broker报告错误: ' + frame.headers['message']); console.log('详细信息: ' + frame.body); } } return ( <BrowserRouter> <Navbar wsConnected={wsConnected}></Navbar> <Routes> <Route exact path="/" element={<NavLayout />}> <Route exact index element={<Mcd stompClient={stompClient.current}/>} /> <Route exact path="location" element={<LocationTool stompClient={stompClient.current} wsConnected={wsConnected}/>} /> <Route exact path="*" element={<NoPage />} /> </Route> </Routes> </BrowserRouter> ); } export default App;
错误信息对比
旧版本错误信息(后端未启动时,每次重试输出)
[HPM] Error occurred while trying to proxy request /ws/info?t=1666207747739 from localhost:3000 to http://localhost:8080 (ECONNREFUSED)
新版本错误信息(后端未启动时,初始输出类似旧版本,多次重试后输出)
[HPM] WebSocket error: Error: read ECONNRESET at TCP.onStreamRead (node:internal/stream_base_commons:217:20) { errno: -4077, code: 'ECONNRESET', syscall: 'read' }
解决方案
1. 正确配置http-proxy-middleware日志过滤
新版本http-proxy-middleware需通过logProvider自定义日志逻辑,实现错误过滤。修改setupProxy.js如下:
const { createProxyMiddleware, logProvider } = require("http-proxy-middleware"); module.exports = function(app) { app.use(createProxyMiddleware("/ws", { target: "http://localhost:8080", ws: true, // 自定义日志提供者,过滤ECONNRESET错误 logProvider: (provider) => { return { ...provider, error: (msg, err) => { // 仅当错误不是ECONNRESET时输出日志 if (!err || err.code !== 'ECONNRESET') { provider.error(msg, err); } } }; } })); }
2. 优化Stomp客户端错误处理
新版本StompJS的错误回调逻辑更细致,可在handleWsErrors中增加对ECONNRESET的判断,避免重复输出日志:
function handleWsErrors(frame) { // 过滤ECONNRESET错误日志 if (frame?.body?.includes('ECONNRESET')) { return; } console.error(' ------------ WS Error! ----------- '); setWsConnected(false); if (typeof(frame) != "undefined") { console.log('Broker报告错误: ' + frame.headers['message']); console.log('详细信息: ' + frame.body); } }
3. 补充说明
- ECONNRESET错误是由于代理尝试与后端WebSocket建立连接时,后端未响应导致连接被重置,多次重试后触发该错误。
- 通过
logProvider自定义日志是http-proxy-middleware v2.x版本的正确配置方式,旧版本的日志配置已不再适用。
内容的提问来源于stack exchange,提问作者John kerich
相关产品推荐
相关产品推荐

