or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

client.mdconnection.mdhandlers.mdindex.mdserver.mdtypes.md
tile.json

handlers.mddocs/

Message Handlers and Control

WebSocket connections can handle control messages (close, ping, pong) through customizable handler functions, and can enable per-message compression for efficient data transfer.

Control Message Handlers

The WebSocket protocol defines three types of control messages: close, ping, and pong. The Conn type provides methods to set and get handlers for these messages.

SetCloseHandler

Sets the handler for close messages received from the peer.

func (c *Conn) SetCloseHandler(h func(code int, text string) error)

The code argument to h is the received close code or CloseNoStatusReceived if the close message is empty. The default close handler sends a close message back to the peer.

The handler function is called from the NextReader, ReadMessage, and message reader Read methods. The application must read the connection to process close messages.

The connection read methods return a CloseError when a close message is received. Most applications should handle close messages as part of their normal error handling. Applications should only set a close handler when the application must perform some action before sending a close message back to the peer.

Example:

conn.SetCloseHandler(func(code int, text string) error {
    log.Printf("Received close message: code=%d, text=%s", code, text)

    // Perform cleanup before closing
    // ...

    // Send close message back to peer
    message := websocket.FormatCloseMessage(code, "")
    conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))
    return nil
})

CloseHandler

Returns the current close handler.

func (c *Conn) CloseHandler() func(code int, text string) error

Example:

handler := conn.CloseHandler()
log.Printf("Current close handler: %T", handler)

SetPingHandler

Sets the handler for ping messages received from the peer.

func (c *Conn) SetPingHandler(h func(appData string) error)

The appData argument to h is the PING message application data. The default ping handler sends a pong to the peer.

The handler function is called from the NextReader, ReadMessage, and message reader Read methods. The application must read the connection to process ping messages.

Example:

conn.SetPingHandler(func(appData string) error {
    log.Printf("Received ping: %s", appData)

    // Send pong response
    err := conn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second))
    if err != nil {
        log.Println("Pong error:", err)
    }
    return err
})

PingHandler

Returns the current ping handler.

func (c *Conn) PingHandler() func(appData string) error

Example:

handler := conn.PingHandler()
log.Printf("Current ping handler: %T", handler)

SetPongHandler

Sets the handler for pong messages received from the peer.

func (c *Conn) SetPongHandler(h func(appData string) error)

The appData argument to h is the PONG message application data. The default pong handler does nothing.

The handler function is called from the NextReader, ReadMessage, and message reader Read methods. The application must read the connection to process pong messages.

If an application sends ping messages, then the application should set a pong handler to receive the corresponding pong.

Example:

conn.SetPongHandler(func(appData string) error {
    log.Printf("Received pong: %s", appData)
    return nil
})

PongHandler

Returns the current pong handler.

func (c *Conn) PongHandler() func(appData string) error

Example:

handler := conn.PongHandler()
log.Printf("Current pong handler: %T", handler)

Compression

The package supports per-message compression extensions (RFC 7692) in an experimental capacity. Compression can reduce bandwidth usage but may increase CPU usage and latency.

EnableWriteCompression

Enables and disables write compression of subsequent text and binary messages.

func (c *Conn) EnableWriteCompression(enable bool)

This function is a noop if compression was not negotiated with the peer during the handshake.

Example:

// Enable compression for outgoing messages
conn.EnableWriteCompression(true)

err := conn.WriteMessage(websocket.TextMessage, []byte("This will be compressed"))
if err != nil {
    log.Println("Write error:", err)
}

// Disable compression
conn.EnableWriteCompression(false)

err = conn.WriteMessage(websocket.TextMessage, []byte("This will not be compressed"))
if err != nil {
    log.Println("Write error:", err)
}

SetCompressionLevel

Sets the flate compression level for subsequent text and binary messages.

func (c *Conn) SetCompressionLevel(level int) error

This function is a noop if compression was not negotiated with the peer. See the compress/flate package for a description of compression levels.

Valid compression levels from the compress/flate package:

  • flate.NoCompression (0): No compression
  • flate.BestSpeed (1): Best speed, least compression
  • flate.BestCompression (9): Best compression, slowest
  • flate.DefaultCompression (-1): Default compression level
  • Values 1-9: Specific compression levels

Example:

import "compress/flate"

// Set compression to best speed
err := conn.SetCompressionLevel(flate.BestSpeed)
if err != nil {
    log.Println("Set compression level error:", err)
}

conn.EnableWriteCompression(true)
err = conn.WriteMessage(websocket.TextMessage, []byte("Compressed at best speed"))
if err != nil {
    log.Println("Write error:", err)
}

// Set compression to best compression
err = conn.SetCompressionLevel(flate.BestCompression)
if err != nil {
    log.Println("Set compression level error:", err)
}

err = conn.WriteMessage(websocket.TextMessage, []byte("Compressed at best compression"))
if err != nil {
    log.Println("Write error:", err)
}

Usage Examples

Keepalive with Ping/Pong

import (
    "time"
    "github.com/gorilla/websocket"
)

func keepalive(conn *websocket.Conn) {
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()

    // Set pong handler to receive responses
    pongReceived := make(chan struct{})
    conn.SetPongHandler(func(appData string) error {
        pongReceived <- struct{}{}
        return nil
    })

    // Start read loop in goroutine
    go func() {
        for {
            if _, _, err := conn.NextReader(); err != nil {
                return
            }
        }
    }()

    for {
        select {
        case <-ticker.C:
            // Send ping
            err := conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second))
            if err != nil {
                log.Println("Ping error:", err)
                return
            }

            // Wait for pong response with timeout
            select {
            case <-pongReceived:
                log.Println("Pong received")
            case <-time.After(5 * time.Second):
                log.Println("Pong timeout")
                conn.Close()
                return
            }
        }
    }
}

Heartbeat with Deadline Reset

func heartbeat(conn *websocket.Conn) {
    const (
        writeWait  = 10 * time.Second
        pongWait   = 60 * time.Second
        pingPeriod = (pongWait * 9) / 10
    )

    // Set initial read deadline
    conn.SetReadDeadline(time.Now().Add(pongWait))

    // Reset deadline on pong
    conn.SetPongHandler(func(string) error {
        conn.SetReadDeadline(time.Now().Add(pongWait))
        return nil
    })

    // Start read loop
    go func() {
        for {
            _, _, err := conn.ReadMessage()
            if err != nil {
                log.Println("Read error:", err)
                return
            }
        }
    }()

    // Send pings periodically
    ticker := time.NewTicker(pingPeriod)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            conn.SetWriteDeadline(time.Now().Add(writeWait))
            if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                log.Println("Ping error:", err)
                return
            }
        }
    }
}

Graceful Shutdown with Close Handler

func gracefulShutdown(conn *websocket.Conn) {
    // Set up close handler
    conn.SetCloseHandler(func(code int, text string) error {
        log.Printf("Received close: code=%d, text=%s", code, text)

        // Perform cleanup
        // ... save state, close resources, etc.

        // Send close response
        message := websocket.FormatCloseMessage(code, "")
        conn.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second))

        return nil
    })

    // Main read loop
    for {
        _, message, err := conn.ReadMessage()
        if err != nil {
            if closeErr, ok := err.(*websocket.CloseError); ok {
                log.Printf("Connection closed: %v", closeErr)
            } else {
                log.Printf("Read error: %v", err)
            }
            break
        }
        // Process message
        log.Printf("Received: %s", message)
    }
}

Conditional Compression

func conditionalCompression(conn *websocket.Conn) {
    // Enable compression if negotiated
    conn.EnableWriteCompression(true)

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

        // Disable compression for small messages
        if len(message) < 1024 {
            conn.EnableWriteCompression(false)
        } else {
            conn.EnableWriteCompression(true)
        }

        err = conn.WriteMessage(websocket.TextMessage, message)
        if err != nil {
            break
        }
    }
}

Custom Close Message

func sendCustomClose(conn *websocket.Conn) {
    // Send custom close message with code and text
    closeMessage := websocket.FormatCloseMessage(
        websocket.CloseNormalClosure,
        "Server shutting down",
    )

    deadline := time.Now().Add(5 * time.Second)
    err := conn.WriteControl(websocket.CloseMessage, closeMessage, deadline)
    if err != nil {
        log.Println("Close error:", err)
    }

    // Close the connection
    conn.Close()
}

Application-Level Ping/Pong

func applicationPingPong(conn *websocket.Conn) {
    // Custom ping/pong with application data
    conn.SetPingHandler(func(appData string) error {
        log.Printf("Ping received with data: %s", appData)

        // Send pong with same data
        deadline := time.Now().Add(5 * time.Second)
        return conn.WriteControl(websocket.PongMessage, []byte(appData), deadline)
    })

    // Read loop
    go func() {
        for {
            if _, _, err := conn.NextReader(); err != nil {
                return
            }
        }
    }()

    // Send ping with custom data
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            appData := []byte("timestamp:" + time.Now().String())
            deadline := time.Now().Add(10 * time.Second)
            err := conn.WriteControl(websocket.PingMessage, appData, deadline)
            if err != nil {
                log.Println("Ping error:", err)
                return
            }
        }
    }
}

Control Message Behavior

Close Messages

Connections handle received close messages by:

  1. Calling the handler function set with SetCloseHandler
  2. Returning a *CloseError from NextReader, ReadMessage, or Read methods

The default close handler sends a close message to the peer.

Ping Messages

Connections handle received ping messages by:

  1. Calling the handler function set with SetPingHandler

The default ping handler sends a pong message to the peer.

Pong Messages

Connections handle received pong messages by:

  1. Calling the handler function set with SetPongHandler

The default pong handler does nothing. If an application sends ping messages, it should set a pong handler to receive the corresponding pong.

Important Notes

Handler Execution Context

Control message handler functions are called from the NextReader, ReadMessage, and message reader Read methods. This means:

  • The application must read the connection to process control messages
  • Handlers can block read operations briefly when writing to the connection
  • Handlers should return quickly to avoid blocking reads

Concurrent Calls

The Close and WriteControl methods can be called concurrently with all other methods, making them safe for use in signal handlers or separate goroutines.

Example:

// Goroutine sending periodic pings
go func() {
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()
    for range ticker.C {
        deadline := time.Now().Add(10 * time.Second)
        conn.WriteControl(websocket.PingMessage, []byte{}, deadline)
    }
}()

// Main goroutine reading messages
for {
    _, message, err := conn.ReadMessage()
    if err != nil {
        break
    }
    // Process message
}

Compression Limitations

Current compression support:

  • Only "no context takeover" modes are supported
  • Messages must be compressed and decompressed in isolation
  • No sliding window or dictionary state is retained across messages
  • Compression is experimental and may result in decreased performance

For more details, refer to RFC 7692.