Search code examples
gowebsocketprotocolsgorilla

Sec-Websocket-Protocol issues


I am facing an issue with Go while using the WebSockets protocol. If I connect to my API, everything works fine. If I add "protocol" such as "Hey", it begins to loop multiple times and finishes by getting an error *github.com/gorilla/websocket.CloseError: "Code 1006, Text Unexpected EOF".

I definitely don't get why it acts like this when I send Sec-Websocket-Protocol among the connection.

There is my code:

main.go

package main

import (
    "fmt"
    "github.com/golang/glog"
    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    stacktracer "gitlab.com/eyes-eyes/internals-stacktracer"
    "gitlab.com/eyesbank/go-web-sockets-server/handlers"
    "net/http"
)

const webSocketsAddr = "0.0.0.0:8082"

// main is the starting point of the current micro service.
func main() {

    // Setting the service name
    stacktracer.SetServiceName("Hello 'X' (Web Sockets)")

    // Initializing the HTTP errors handling
    runtime.HTTPError = stacktracer.DefaultHTTPError

    if err := RunWebSocketsServer(); err != nil {
        glog.Fatal(err)
    }

}

//
// WebSocket
//

func RunWebSocketsServer() error {

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        handlers.HandleUserSocket(w, r)
    })

    fmt.Println(webSocketsAddr)

    return http.ListenAndServe(webSocketsAddr, nil)
}

func RunWebSocketsTLSServer() error {

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        handlers.HandleUserSocket(w, r)
    })

    fmt.Println(webSocketsAddr)

    return http.ListenAndServeTLS(webSocketsAddr, "server.crt", "server.key", nil)
}

handler.go

package handlers

import (
    "fmt"
    "github.com/gorilla/websocket"
    stacktracer "gitlab.com/eyes-eyes/internals-stacktracer"
    "gitlab.com/eyes-eyes/internals-stacktracer/structs"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "log"
    "net/http"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

func HandleUserSocket(w http.ResponseWriter, r *http.Request) {

    var userID = primitive.NewObjectID()
    conn, err := upgrader.Upgrade(w, r, nil) // error ignored for sake of simplicity
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    } else {
        WriteOutgoingMessage(conn, "Welcome "+userID.Hex())
    }

    fmt.Println(r.Header["Sec-Websocket-Protocol"])
    if len(r.Header["Sec-Websocket-Protocol"]) > 0 {
        WriteOutgoingMessage(conn, userID.Hex() + " " + string(r.Header["Sec-Websocket-Protocol"][0]))
    }

    for {
        // Read message from browser
        _, msg, err := conn.ReadMessage()
        if err != nil {
            if err != nil {
                switch err.(type) {
                case *websocket.CloseError:
                    fmt.Println("disconnected")
                    return
                default:
                    _ = conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
                    fmt.Println(err.Error())
                    fmt.Println("disconnected")
                    return
                }
            }
        }

        if msg != nil {
            WriteOutgoingMessage(conn, userID.Hex() + " " + string(msg))
        }
    }
}

func WriteOutgoingMessage(conn *websocket.Conn, message string) *structs.StackTrace {

    // Write message back to browser
    if err := conn.WriteMessage(websocket.TextMessage, []byte("Got: \""+message+"\"")); err != nil {
        err = conn.WriteMessage(websocket.TextMessage, []byte(err.Error()))
        if err != nil {
            return stacktracer.NewStackTrace(500, err.Error(), nil)
        }
    }

    return nil

}

Solution

  • If a client requests subprotocols and the server does not agree to one of those subprotocols, then the client is required to close the connection. A client uses the Sec-Websocket-Protocol header to request one or more subprotocols. A server uses the Sec-Websocket-Protocol response header to agree to a protocol. the See the RFC for more on this topic.

    Fix the problem by agreeing to one of the protocols requested by the client. There are a couple of ways to do this.

    The first is to use the built-in protocol negotiation feature:

    var upgrader = websocket.Upgrader{
        ReadBufferSize:  1024,
        WriteBufferSize: 1024,
        Subprotocols: []string{ "hey" },  // <-- add this line
        CheckOrigin: func(r *http.Request) bool {
            return true
        },
    }
    

    The second is to negotiate the protocol in application code before the call to Upgrade. Call websocket.Subprotocols to get the requested protocols, select one of the protocols and specify that protocol in the header argument to Upgrade.

    h := http.Header{}
    for _, sub := range websocket.Subprotocols(req) {
       if sub == "hey" {
          h.Set("Sec-Websocket-Protocol", "hey")
          break
       }
    }
    conn, err := upgrader.Upgrade(w, r, h)
    

    Separate from this issue, the application should defer conn.Close() after successful upgrade.

    Also, the error handling logic can be simplified. The application should exit the read loop on any error returned from ReadMessage. There's no point in writing a message after the connection errors. The ReadMessage method returns a non-nil message on success.

    for {
        // Read message from browser
        _, msg, err := conn.ReadMessage()
        if err != nil {
             fmt.Println(err.Error())
              fmt.Println("disconnected")
              return
        }
        WriteOutgoingMessage(conn, userID.Hex() + " " + string(msg))
    }