binance websocket not responding to ping

Issue

Using gorilla/websocket I dial the binance websocket endpoint, which succeeds without error. After setting the pong handler on the connection, I write a ping control message and wait for a pong to arrive at the pong handler, which never seems to happen. I use a channel, a context with timeout and a select block to check if the pong arrived.

The code:

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/gorilla/websocket"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    conn, resp, err := websocket.DefaultDialer.DialContext(ctx, "wss://stream.binance.com:9443/ws", nil)
    if resp != nil {
        log.Printf("status: %s", resp.Status)
    }

    if err != nil {
        panic(err)
    }

    pong := make(chan struct{})
    conn.SetPongHandler(func(appData string) error {
        log.Println(appData)

        pong <- struct{}{}
        return nil
    })

    if err := conn.WriteControl(websocket.PingMessage, []byte("Hello, world!"), time.Now().Add(5*time.Second)); err != nil {
        panic(err)
    }

    select {
    case <-ctx.Done():
        panic(fmt.Errorf("pong wait: %w", ctx.Err()))
    case <-pong:
    }
}

Output:

$ go run ./cmd/ping
2022/02/07 20:01:23 status: 101 Switching Protocols
panic: pong wait: context deadline exceeded

goroutine 1 [running]:
main.main()
        /workspaces/yatgo/cmd/ping/ping.go:39 +0x2ba
exit status 2

As per rfc6455, section-5.5.2:

5.5.2. Ping

The Ping frame contains an opcode of 0x9.

A Ping frame MAY include "Application data".

Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
response, unless it already received a Close frame. It SHOULD
respond with Pong frame as soon as is practical. Pong frames are
discussed in Section 5.5.3.

An endpoint MAY send a Ping frame any time after the connection is
established and before the connection is closed.

NOTE: A Ping frame may serve either as a keepalive or as a means to
verify that the remote endpoint is still responsive.

I kind off expected this to work. Binance websocket API limits doc does mentions ping messages:

WebSocket connections have a limit of 5 incoming messages per second. A message is considered:

  • A PING frame

So I wonder:

  1. Is something wrong with my code?
  2. Or is binance not respecting RFC6455?

Solution

The Gorilla Websocket documentation says:

The application must read the connection to process close, ping and pong messages sent from the peer. If the application is not otherwise interested in messages from the peer, then the application should start a goroutine to read and discard messages from the peer.

Fix the application by starting a goroutine to read the connection before the select statement:

go func() {
    defer cancel()
    for {
        if _, _, err := conn.NextReader(); err != nil {
            fmt.Println(err)
            return
        }
    }
}()

select {
⋮

This is a fix for the application shown in the question. If your actual application reads data from the connection in a loop, then you should not add the goroutine shown here. The application should use one read loop to handle control and data messages.

Answered By – Bayta Darell

Answer Checked By – Pedro (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.