gobwas / ws

Tiny WebSocket library for Go.
MIT License
6.1k stars 373 forks source link

Client disconnects on its own due to the 1008 error and EOF error. #200

Closed yzddd closed 4 months ago

yzddd commented 4 months ago

After establishing a WebSocket connection to an address, the connection is immediately dropped, and an EOF error is encountered. After multiple attempts, it seems that the reason may be that the client sends a certain request or handshake to the server, causing the client to disconnect on its own, triggering the 1008 error. Here is the code and output.I'm hoping for some assistance, your help would be greatly appreciated. Thank you!

Code:

func` TestGobwasWs(t *testing.T) {
    u, err := url.Parse("wss://ws-fapi.binance.com/ws-fapi/v1")
    if err != nil {
        log.Fatalf("failed to parse URL: %v", err)
    }

    conn, _, _, err := ws.Dial(context.Background(), u.String())
    if err != nil {
        log.Fatalf("failed to dial: %v", err)
    }

    defer conn.Close()

    go func() {
        for {
            msg, op, err := wsutil.ReadServerData(conn)
            if err != nil {
                log.Printf("failed to read message: %v", err)
                break
            }

            fmt.Printf("Received message: %s\n", string(msg))

            if err := wsutil.WriteClientMessage(conn, op, msg); err != nil {
                log.Printf("failed to write message: %v", err)
                break
            }
        }
    }()
    go func() {
        for {
            ts := fmt.Sprintf("%d", time.Now().Unix()*1000)
            pingMsg := map[string]interface{}{
                "id":     ts,
                "method": "ping",
            }
            msg, _ := json.Marshal(pingMsg)

            // msg = []byte("Hello, World!")
            err := wsutil.WriteClientMessage(conn, ws.OpText, msg)
            if err != nil {
                log.Printf("failed to write message: %v", err)
                break
            }

            time.Sleep(5 * time.Second)
        }
    }()
    // Keep the main goroutine running
    select {}
}

OUTPUT: Running tool: /home/ubuntu/go/bin/go test -timeout 30s -run ^TestGobwasWs$ === RUN TestGobwasWs Received message: {"id":"1714779322000","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":6}]} Received message: {"id":"1714779322000","status":400,"error":{"code":-1000,"msg":"Missing required 'method' in JSON request."}} 2024/05/04 07:35:22 failed to read message: ws closed: 1008 disconnected 2024/05/04 07:35:27 failed to write message: write tcp 10.0.4.10:58838->18.182.135.180:443: write: broken pipe

cristaloleg commented 4 months ago

Hey, which version do you use?

yzddd commented 4 months ago

Hey, which version do you use?

Hi!. It is gobwas/ws v1.3.0

github.com/gobwas/ws v1.3.0
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
cristaloleg commented 4 months ago

Looks like the connection is closed on Binance side due to error in the payload, see message:

Received message: {"id":"1714803365000","status":400,"error":{"code":-1000,"msg":"Missing required 'method' in JSON request."}}
yzddd commented 4 months ago

Looks like the connection is closed on Binance side due to error in the payload, see message:

Received message: {"id":"1714803365000","status":400,"error":{"code":-1000,"msg":"Missing required 'method' in JSON request."}}

Hi! Mr. Kovalov, I think I sent only one "ping" request and received a correct 200 response, as expected. However, I later received a 400 response indicating that the method was missing. I was confused.

I actually used gobwas‘ ”dialer.Dial“ function to connect to this address and establish a WebSocket connection. However, as soon as the connection is established, it gets disconnected, and I keep receiving EOF. This issue only occurs with this specific address; other addresses work fine. The original codebase is quite extensive, with many layers of abstraction. I spent an entire day troubleshooting but couldn't pinpoint the problem.

To validate, I tried using gorilla/websocket as a comparison, and I didn't encounter a similar issue. That's why I created this simple test code in the issue to see if I can reproduce this bug.

cristaloleg commented 4 months ago

Check the 2nd if in 1st goroutine. You send back what you've received from Binance:

{"id":"1714824103000","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":12}]}

In this payload there is no requested field 'method' as Binance says. Do you've a gorilla/websocket reproducer that does exactly the same steps and receive something else in a response?

yzddd commented 4 months ago

Do you've a gorilla/websocket reproducer that does exactly the same steps and receive something else in a response?

Hi, Yes, I tried the same code on Gorilla/WebSocket, and it seems to be working fine. Code

func TestGorillaWs(t *testing.T) {
    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)

    // WebSocket地址
    //url := "wss://stream.binance.com:9443/ws/btcusdt@depth"
    url := "wss://ws-fapi.binance.com/ws-fapi/v1"
    // 连接到WebSocket地址
    c, _, err := websocket.DefaultDialer.Dial(url, nil)
    if err != nil {
        log.Fatal("dial:", err)
    }
    defer c.Close()

    done := make(chan struct{})

    go func() {
        defer close(done)
        for {
            _, message, err := c.ReadMessage()
            if err != nil {
                log.Println("read:", err)
                return
            }
            fmt.Printf("Received: %s\n", message)
        }
    }()

    go func() {
        for {
            ts := fmt.Sprintf("%d", time.Now().Unix()*1000)
            pingMsg := map[string]interface{}{
                "id":     ts,
                "method": "ping",
            }
            msg, err := json.Marshal(pingMsg)
            if err != nil {
                log.Println("json marshal error:", err)
                return
            }
            err = c.WriteMessage(websocket.TextMessage, msg)
            if err != nil {
                log.Println("write ping message error:", err)
            }
            time.Sleep(5 * time.Second)
        }
    }()
    select {}
}

output:

Running tool: /home/ubuntu/go/bin/go test -timeout 30s -run ^TestGorillaWs$
=== RUN   TestGorillaWs
Received: {"id":"1714825512000","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":6}]}
Received: {"id":"1714825517000","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":7}]}
Received: {"id":"1714825522000","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":8}]}
Received: {"id":"1714825527000","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":9}]}
Received: {"id":"1714825532000","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":10}]}
Received: {"id":"1714825537000","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":2400,"count":11}]}
cristaloleg commented 4 months ago

As mentioned above: You've an explicit send of received data:

            if err := wsutil.WriteClientMessage(conn, op, msg); err != nil {
                log.Printf("failed to write message: %v", err)
                break
            }

Tried to add similar code in gorilla/websocket example and it will fail:

            err = c.WriteMessage(websocket.TextMessage, message)
            if err != nil {
                log.Println("write ping message error:", err)
            }
yzddd commented 4 months ago

Oh! Thank you, this is indeed where the issue lies. Thanks for your kind help!