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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 23:35:20