gowebsocketgorillascs

How to implement http.Hijacker using Gorilla Websockets & alexedwards/scs/v2


Recently upgraded the excellent alexedwards/scs/v2 within a Go webapp from 2.5.0 to 2.7.0 to allow for Go 1.20+ support for http.NewResponseController(). Subsequently to allow for extending handler specific server write timeouts for large file uploads. This is good. An unfortunate consequence of this necessary upgrade is the loss of websocket functionality, currently using Gorilla websockets v1.5.1 (latest). I get the common error produced on attempted connection upgrade... websocket: response does not implement http.Hijacker I've researched several leads for this, a few here on SO but I can't resolve anything yet which is specific to this case. I've tried a few attempts at implementing http.Hijacker alongside my other middleware code and within my websocket endpoint handler (commented out, 'misguided' example attached for illustration)... I just don't think I understand this well enough...

type WebsocketConnection struct {
    *websocket.Conn
}

type WebsocketPayload struct {
    Action      string              `json:"action"`
    Message     string              `json:"message"`
    AdminName   string              `json:"admin_name"`
    MessageType string              `json:"message_type"`
    AdminID     int                 `json:"admin_id"`
    Connection  WebsocketConnection `json:"-"`
}

type WebsocketJSONResponse struct {
    Action        string   `json:"action"`
    Message       string   `json:"message"`
    AdminName     string   `json:"admin_name"`
    AdminID       int      `json:"admin_id"`
    CurrentLogins []string `json:"current_logins"`
}

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

var webClients = make(map[WebsocketConnection]string)

var websocketChannel = make(chan WebsocketPayload)

func (app *application) WebsocketEndPoint(w http.ResponseWriter, r *http.Request) {
    // rc := http.NewResponseController(w)
    // netConn, _, err := rc.Hijack()
    // if err != nil {
    //  app.errorLog.Println(err)
    //  return
    // }
    // defer netConn.Close()

    app.debugLog.Printf("w's type is %T\n", w)

    ws, err := upgradeConnection.Upgrade(w, r, nil)
    if err != nil {
        app.errorLog.Println(err)  <= ERROR HERE
        return
    }

    app.infoLog.Printf("Web client connected from %s", r.RemoteAddr)
    var response WebsocketJSONResponse
    response.Message = "Connected to server..."

    err = ws.WriteJSON(response)
    if err != nil {
        app.errorLog.Println(err)
        return
    }

    conn := WebsocketConnection{Conn: ws}
    webClients[conn] = ""

    go app.ListenForWebsocket(&conn)
}

"w" in this case is *scs.sessionResponseWriter. How do I implement http.Hijacker in the response without wrecking the connection and get the upgrade made ? What is the correct approach please ?


Solution

  • Version 2.5.0 of the alexedwards/scs/v2 package supports the Hijacker interface. Version 2.7.0 does not support the Hijacker interface, but it does support support response controllers. The gorilla websocket package does not use response controllers.

    Option 1

    Use 2.5.0 of the alexedwards/scs/v2 package.

    Option 2

    Work around the gorilla package's defect by unwrapping the response writer in your application code:

    func (app *application) WebsocketEndPoint(w http.ResponseWriter, r *http.Request) {
    
        wupgrade := w
        if u, ok := w.(interface{ Unwrap() http.ResponseWriter }); ok {
            wupgrade = u.Unwrap()
        }
    
        app.debugLog.Printf("w's type is %T\n", w)
    
        ws, err := upgradeConnection.Upgrade(wupgrade, r, nil)
        if err != nil {
            app.errorLog.Println(err)
            return
        }
    
        app.infoLog.Printf("Web client connected from %s", r.RemoteAddr)
        var response WebsocketJSONResponse
        response.Message = "Connected to server..."
    
        err = ws.WriteJSON(response)
        if err != nil {
            app.errorLog.Println(err)
            return
        }
    
        conn := WebsocketConnection{Conn: ws}
        webClients[conn] = ""
    
        go app.ListenForWebsocket(&conn)
    }