WebSocket

Hijack-based WebSocket API reference. The current native Linux io_uring worker runtime does not support WebSocket upgrades because they rely on connection hijacking.

Upgrading Connections

Upgrade an HTTP request to a WebSocket connection using UpgradeWebSocket(). This hijacks the underlying TCP connection:

WebSocket upgrade is currently a compatibility feature. The native Linux io_uring worker backends reject hijacked connections, so this API is not available on that production path yet.

go
s.Router.GET("/ws", func(req *core.Request, resp *core.Response) {
    ws := core.UpgradeWebSocket(req, resp)
    if ws == nil {
        return
    }
    defer ws.Close()
})

The upgrade validates:

  • Upgrade: websocket header
  • Connection: Upgrade header
  • Sec-WebSocket-Version: 13
  • Sec-WebSocket-Key present

If any check fails, nil is returned and a 400 Bad Request is sent.

Echo Server Example

The simplest WebSocket handler — echo back every message received:

go
s.Router.GET("/ws/echo", func(req *core.Request, resp *core.Response) {
    ws := core.UpgradeWebSocket(req, resp)
    if ws == nil {
        return
    }
    defer ws.Close()

    for {
        opcode, data, err := ws.ReadMessage()
        if err != nil {
            break
        }

        if err := ws.WriteMessage(opcode, data); err != nil {
            break
        }
    }
})

Reading & Writing Messages

Reading

ReadMessage() returns the opcode, data, and error. It handles frame fragmentation, unmasking, and control frames automatically:

go
opcode, data, err := ws.ReadMessage()

switch opcode {
case 0x1:
    msg := string(data)
    fmt.Println("Text:", msg)
case 0x2:
    fmt.Println("Binary:", len(data), "bytes")
}

Writing

Send messages with specific types:

go
ws.WriteText("Hello from the server!")

ws.WriteBinary(imageBytes)

ws.WriteMessage(0x1, []byte("text message"))
ws.WriteMessage(0x2, binaryData)

Frame Types (Opcodes)

Opcode Type Description
0x0 Continuation Continues a fragmented message
0x1 Text UTF-8 text frame
0x2 Binary Binary data frame
0x8 Close Connection close (auto-echoed)
0x9 Ping Ping (auto-responds with Pong)
0xA Pong Pong response

Automatic handling: PING frames are automatically responded with PONG. CLOSE frames are echoed with the status code per RFC 6455 §5.5.1. You don't need to handle control frames manually.

Ping / Pong

Send manual pings to check the connection is alive:

go
if err := ws.Ping(); err != nil {
    return
}

Deadlines

Set read/write deadlines on the WebSocket connection:

go
ws.SetDeadline(time.Now().Add(30 * time.Second))

for {
    ws.SetDeadline(time.Now().Add(30 * time.Second))
    opcode, data, err := ws.ReadMessage()
    if err != nil {
        break
    }
    handleMessage(opcode, data)
}

Chat Room Example

A more complete example showing a broadcast-style chat room:

go
var (
    clients = make(map[*core.WSConn]bool)
    mu      sync.RWMutex
)

func broadcast(msg string) {
    mu.RLock()
    defer mu.RUnlock()
    for ws := range clients {
        ws.WriteText(msg)
    }
}

s.Router.GET("/ws/chat", func(req *core.Request, resp *core.Response) {
    ws := core.UpgradeWebSocket(req, resp)
    if ws == nil {
        return
    }

    mu.Lock()
    clients[ws] = true
    mu.Unlock()

    defer func() {
        mu.Lock()
        delete(clients, ws)
        mu.Unlock()
        ws.Close()
    }()

    for {
        _, data, err := ws.ReadMessage()
        if err != nil {
            break
        }
        broadcast(string(data))
    }
})

WSConn Methods Reference

Method Description
ReadMessage() Read next message (opcode, data, error)
WriteMessage(op, data) Write frame with opcode
WriteText(msg) Write text frame (0x1)
WriteBinary(data) Write binary frame (0x2)
Ping() Send ping frame
Close() Close the connection
SetDeadline(t) Set read/write deadline

JSON over WebSocket

Parse and send JSON messages over WebSocket connections:

go
type Message struct {
    Type    string `json:"type"`
    Payload string `json:"payload"`
}

s.Router.GET("/ws/json", func(req *core.Request, resp *core.Response) {
    ws := core.UpgradeWebSocket(req, resp)
    if ws == nil {
        return
    }
    defer ws.Close()

    for {
        _, data, err := ws.ReadMessage()
        if err != nil {
            break
        }

        var msg Message
        if err := json.Unmarshal(data, &msg); err != nil {
            ws.WriteText(`{"error":"invalid json"}`)
            continue
        }

        response, _ := json.Marshal(Message{
            Type:    "response",
            Payload: "received: " + msg.Payload,
        })
        ws.WriteText(string(response))
    }
})

Heartbeat Pattern

Keep connections alive with periodic pings in a goroutine:

go
s.Router.GET("/ws/heartbeat", func(req *core.Request, resp *core.Response) {
    ws := core.UpgradeWebSocket(req, resp)
    if ws == nil {
        return
    }
    defer ws.Close()

    done := make(chan struct{})
    go func() {
        ticker := time.NewTicker(15 * time.Second)
        defer ticker.Stop()
        for {
            select {
            case <-ticker.C:
                if err := ws.Ping(); err != nil {
                    return
                }
            case <-done:
                return
            }
        }
    }()

    for {
        ws.SetDeadline(time.Now().Add(30 * time.Second))
        opcode, data, err := ws.ReadMessage()
        if err != nil {
            break
        }
        ws.WriteMessage(opcode, data)
    }
    close(done)
})

Binary Messages

Handle binary data (images, protocol buffers, etc.):

go
s.Router.GET("/ws/binary", func(req *core.Request, resp *core.Response) {
    ws := core.UpgradeWebSocket(req, resp)
    if ws == nil {
        return
    }
    defer ws.Close()

    for {
        opcode, data, err := ws.ReadMessage()
        if err != nil {
            break
        }

        if opcode == 0x2 {
            processed := processImage(data)
            ws.WriteBinary(processed)
        } else {
            ws.WriteText("send binary data only")
        }
    }
})

Combine text commands with binary data by checking the opcode:

go
for {
    opcode, data, err := ws.ReadMessage()
    if err != nil {
        break
    }

    switch opcode {
    case 0x1:
        handleCommand(string(data))
    case 0x2:
        handleUpload(data)
    }
}
ESC