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

Node.js中Socket.IO v2.5.0客户端WebSocket 403错误的原因与解决

Node.js Socket.IO客户端WebSocket 403错误排查与解决

问题描述

使用Node.js运行基于Socket.IO v2.5.0和socket.io-wildcard库的客户端代码时,终端返回Unexpected server response: 403错误,但相同逻辑在浏览器中运行正常,需排查错误成因并找到解决方案。

终端报错信息

Error: websocket error
    at Transport.onError (/home/BoMbO/node_modules/engine.io-client/lib/transport.js:68:13)
    at ws.onerror (/home/BoMbO/node_modules/engine.io-client/lib/transports/websocket.js:163:10)
    at WebSocket.onError (/home/BoMbO/node_modules/ws/lib/event-target.js:140:16)
    at WebSocket.emit (node:events:513:28)
    at abortHandshake (/home/BoMbO/node_modules/ws/lib/websocket.js:731:15)
    at ClientRequest.<anonymous> (/home/BoMbO/node_modules/ws/lib/websocket.js:604:7)
    at ClientRequest.emit (node:events:513:28)
    at HTTPParser.parserOnIncomingClient [as onIncoming] (node:_http_client:701:27)
    at HTTPParser.parserOnHeadersComplete (node:_http_common:119:17)
    at TLSSocket.socketOnData (node:_http_client:542:22) {
  type: 'TransportError',
  description: ErrorEvent {
    target: WebSocket {
      _events: [Object: null prototype],
      _eventsCount: 4,
      _maxListeners: undefined,
      _binaryType: 'arraybuffer',
      _closeCode: 1006,
      _closeFrameReceived: false,
      _closeFrameSent: false,
      _closeMessage: '',
      _closeTimer: null,
      _extensions: {},
      _protocol: '',
      _readyState: 2,
      _receiver: null,
      _sender: null,
      _socket: null,
      _bufferedAmount: 0,
      _isServer: false,
      _redirects: 0,
      _url: 'wss://ws2.basqu.com/socket.io/?EIO=3&transport=websocket',
      _req: [ClientRequest],
      [Symbol(kCapture)]: false
    },
    type: 'error',
    message: 'Unexpected server response: 403',
    error: Error: Unexpected server response: 403
        at ClientRequest.<anonymous> (/home/BoMbO/node_modules/ws/lib/websocket.js:604:7)
        at ClientRequest.emit (node:events:513:28)
        at HTTPParser.parserOnIncomingClient [as onIncoming] (node:_http_client:701:27)
        at HTTPParser.parserOnHeadersComplete (node:_http_common:119:17)
        at TLSSocket.socketOnData (node:_http_client:542:22)
        at TLSSocket.emit (node:events:513:28)
        at addChunk (node:internal/streams/readable:324:12)
        at readableAddChunk (node:internal/streams/readable:297:9)
        at Readable.push (node:internal/streams/readable:234:10)
        at TLSWrap.onStreamRead (node:internal/stream_base_commons:190:23)
  }
}

Node.js客户端代码

const socket = require('socket.io-client')('wss://ws2.basqu.com/', {
    path: '/socket.io',
    reconnection: true,
    // options of the Engine.IO client
    transports: ['websocket'],
    transportOptions: {
        websocket: {
            extraHeaders: {
                origin: 'https://basqu.com'
            }
        }
    },
    upgrade: false,
    rememberUpgrade: true,
    // options for Node.js
    rejectUnauthorized: false,
    extraHeaders: {
         origin: 'https://basqu.com'
    }
});

socket.on('connect', () =>{
    const sid = socket.id;
    console.log(`Connected with session ID: ${sid}`)

    socket.emit("authorization",{"session":"FlaqiaG...iiii..loveee.you....OX","isDemo":1});

    socket.on("instruments/list", () => {
        console.log('received: instruments/list')
    });

    socket.on("s_authorization",() => {
        console.log('received: s_authorization')
    });

    let lastTickTime = 0; // track the last time "tick" was sent

    function sendTick() {
      const now = Date.now();
      const elapsedTime = now - lastTickTime;

      if (elapsedTime >= 60000) { // send "tick" if at least 1 minute has elapsed
        socket.emit('tick');
        lastTickTime = now;
      }
    }
    setInterval(sendTick, 500);
    
    socket.on("orders/closed/list",() => {
        console.log('received orders/closed/list')
    });
    socket.on("orders/opened/list",() => {
        console.log('received order/opened/list')
    });

    socket.emit("indicator/list");

    socket.emit("drawing/load");

    socket.emit("pending/list");

    socket.emit("instruments/update",{
        "asset":"AUDCAD_otc",
        "period":60
    });

    socket.on("s_drawing/load", () => {
        console.log('received: s_drawing/load')
    });

    socket.on("settings/list",() => {
        console.log('received: settings/list')
    });

    socket.on("s_balance/list",() => {
        console.log('received: s_balance/list')
    });
    socket.on("s_indicator/list",() => {
        console.log('received: s_indicator/list')
    });

    socket.on("s_chart_notification/get",() => {
        console.log('received: s_chart_notification/get')
    });

    socket.on("pending/list",() => {
        console.log('received: pending/list')
    });
   
    socket.on("history/list/v2",() => {
        console.log('received: history/list/v2')
    });

    socket.on("quotes/stream",(data) => {
        console.log(`received quotes/stream event with data: ${data}`);

        const utf8Data = new TextDecoder().decode(data);

        console.log("Received UTF-8 encoded data: ", utf8Data);
    });

    socket.on("depth/change",() => {
        console.log('received: depth/change')
    });

    socket.emit("chart_notification/get");

    socket.emit("depth/follow","AUDCAD_otc");

    socket.emit("settings/store",{"chartId":"graph","settings":{"chartId":"graph","chartType":2,"currentExpirationTime":0,"isFastOption":false,"isFastAmountOption":false,"isIndicatorsMinimized":false,"isIndicatorsShowing":true,"isShortBetElement":false,"chartPeriod":4,"currentAsset":{"symbol":"AUDCAD_otc"},"dealValue":1000,"dealPercentValue":1,"isVisible":true,"timePeriod":60,"gridOpacity":8,"isAutoScrolling":1,"isOneClickTrade":true,"upColor":"#0FAF59","downColor":"#FF6251"}});

});


socket.on('*',(event,data) => {
    console.log('Received event: ${event}, with data: ${data}')
});

socket.on('disconnect', (reason) => {
    console.log(reason)
});

socket.on('connect_error',(err) =>{
    console.log(err)
})

错误成因分析

  1. 请求头差异:浏览器发起WebSocket请求时会自动附带User-Agent、Referer等默认请求头,Node.js客户端默认不包含这些头,服务器可能以此识别非浏览器请求并返回403拦截。
  2. 传输方式强制限制:代码中设置transports: ['websocket']并禁用升级(upgrade: false),而浏览器通常先通过HTTP Polling建立连接再升级到WebSocket,服务器可能对直接WebSocket握手有额外验证逻辑。
  3. 跨域验证逻辑:服务器的CORS验证可能不仅检查Origin头,还会结合其他请求头或会话信息,Node.js客户端未完全模拟浏览器的请求头组合。
  4. 握手阶段凭证缺失:服务器可能在WebSocket握手阶段就需要验证会话凭证(如Cookie),但当前代码仅在连接成功后发送authorization事件,未在握手时传递必要凭证。

解决方案

1. 补充完整请求头

在客户端配置的extraHeaders中添加浏览器常用的请求头,模拟浏览器请求:

extraHeaders: {
    'Origin': 'https://basqu.com',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
    'Referer': 'https://basqu.com/'
}

注意:全局extraHeaders会覆盖transportOptions.websocket.extraHeaders,只需设置一处即可。

2. 恢复默认传输升级逻辑

去掉upgrade: false,允许客户端先通过HTTP Polling建立连接,再升级到WebSocket,和浏览器行为保持一致:

// 移除或注释掉这行
// upgrade: false,

3. 传递握手阶段所需凭证

如果服务器在握手时需要Cookie或其他凭证,可在extraHeaders中添加对应的头,例如:

extraHeaders: {
    'Origin': 'https://basqu.com',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
    'Cookie': 'your_session_cookie_here'
}

4. 调试请求差异

使用抓包工具(如Charles、Wireshark)对比浏览器和Node.js客户端的WebSocket握手请求,找出服务器验证所需的关键请求头,补充到客户端配置中。

5. 确认版本兼容性

确保客户端使用的Socket.IO版本(v2.5.0)与服务器端版本完全兼容,版本不匹配可能导致握手协议不一致,引发403错误。


内容的提问来源于stack exchange,提问作者Exboy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 01:47:38