Skip to main content

Overview

The websocket middleware handles WebSocket upgrade requests, enabling real-time bidirectional communication between clients and your server.

Installation

import "github.com/go-mizu/mizu/middlewares/websocket"

Quick Start

app := mizu.New()

app.Get("/ws", websocket.New(func(c *mizu.Ctx, ws *websocket.Conn) error {
    for {
        msgType, data, err := ws.ReadMessage()
        if err != nil {
            return err
        }

        // Echo message back
        if err := ws.WriteMessage(msgType, data); err != nil {
            return err
        }
    }
}))

Configuration

OptionTypeDefaultDescription
Origins[]stringAll allowedAllowed origin domains
Subprotocols[]string-Supported subprotocols
CheckOriginfunc(*http.Request) bool-Custom origin validator

Examples

Echo Server

app.Get("/ws", websocket.New(func(c *mizu.Ctx, ws *websocket.Conn) error {
    for {
        _, data, err := ws.ReadMessage()
        if err != nil {
            return err // Client disconnected
        }

        if err := ws.WriteText(string(data)); err != nil {
            return err
        }
    }
}))

Chat Application

var clients = make(map[*websocket.Conn]bool)
var broadcast = make(chan string)
var mu sync.Mutex

// Broadcast messages to all clients
go func() {
    for msg := range broadcast {
        mu.Lock()
        for client := range clients {
            client.WriteText(msg)
        }
        mu.Unlock()
    }
}()

app.Get("/chat", websocket.New(func(c *mizu.Ctx, ws *websocket.Conn) error {
    // Register client
    mu.Lock()
    clients[ws] = true
    mu.Unlock()

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

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

Origin Validation

app.Get("/ws", websocket.WithOptions(
    func(c *mizu.Ctx, ws *websocket.Conn) error {
        // Handler
    },
    websocket.Options{
        Origins: []string{
            "https://example.com",
            "https://app.example.com",
        },
    },
))

Custom Origin Check

app.Get("/ws", websocket.WithOptions(
    handler,
    websocket.Options{
        CheckOrigin: func(r *http.Request) bool {
            origin := r.Header.Get("Origin")
            // Custom validation logic
            return strings.HasSuffix(origin, ".example.com")
        },
    },
))

Subprotocols

app.Get("/ws", websocket.WithOptions(
    handler,
    websocket.Options{
        Subprotocols: []string{"graphql-ws", "subscriptions-transport-ws"},
    },
))

Binary Messages

app.Get("/ws", websocket.New(func(c *mizu.Ctx, ws *websocket.Conn) error {
    for {
        msgType, data, err := ws.ReadMessage()
        if err != nil {
            return err
        }

        switch msgType {
        case websocket.TextMessage:
            ws.WriteText("Received text: " + string(data))
        case websocket.BinaryMessage:
            ws.WriteBinary(processBinary(data))
        }
    }
}))

Ping/Pong

app.Get("/ws", websocket.New(func(c *mizu.Ctx, ws *websocket.Conn) error {
    // Send periodic pings
    go func() {
        ticker := time.NewTicker(30 * time.Second)
        defer ticker.Stop()
        for range ticker.C {
            if err := ws.Ping(nil); err != nil {
                return
            }
        }
    }()

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

        // Handle pong automatically
        if msgType == websocket.PongMessage {
            continue
        }

        ws.WriteMessage(msgType, data)
    }
}))

Message Types

ConstantValueDescription
TextMessage1UTF-8 text data
BinaryMessage2Binary data
CloseMessage8Close frame
PingMessage9Ping frame
PongMessage10Pong frame

Conn Methods

func (c *Conn) ReadMessage() (messageType int, data []byte, err error)
func (c *Conn) WriteMessage(messageType int, data []byte) error
func (c *Conn) WriteText(text string) error
func (c *Conn) WriteBinary(data []byte) error
func (c *Conn) Ping(data []byte) error
func (c *Conn) Pong(data []byte) error
func (c *Conn) Close() error

API Reference

func New(handler Handler) mizu.Middleware
func WithOptions(handler Handler, opts Options) mizu.Middleware
func IsWebSocketUpgrade(r *http.Request) bool

type Handler func(c *mizu.Ctx, ws *Conn) error

Client Example

const ws = new WebSocket('ws://localhost:8080/ws');

ws.onopen = () => {
    ws.send('Hello, server!');
};

ws.onmessage = (event) => {
    console.log('Received:', event.data);
};

ws.onclose = () => {
    console.log('Connection closed');
};

Technical Details

WebSocket Protocol Implementation

The middleware implements the WebSocket protocol (RFC 6455) with the following key components:

Connection Upgrade Process

  1. Request Validation: Checks for Upgrade: websocket and Connection: Upgrade headers
  2. Origin Validation: Validates the request origin against configured allowed origins
  3. Key Exchange: Validates Sec-WebSocket-Key and computes the accept key using SHA1 hash with the WebSocket GUID
  4. Subprotocol Negotiation: Matches requested subprotocols with supported ones
  5. HTTP Hijacking: Takes over the HTTP connection using Go’s http.Hijacker interface
  6. 101 Switching Protocols: Sends the upgrade response with Sec-WebSocket-Accept header

Frame Structure

The implementation handles WebSocket frames with the following structure:
  • Opcode: Identifies the frame type (text, binary, close, ping, pong)
  • Masking: Client-to-server messages must be masked; server-to-client messages are unmasked
  • Payload Length: Supports three length encoding formats:
    • 0-125 bytes: Single byte length
    • 126-65535 bytes: 2-byte extended length
    • 65536+ bytes: 8-byte extended length

Connection Management

  • Thread-Safe Writes: Uses mutex locks to ensure concurrent write safety
  • Buffered I/O: Utilizes bufio.Reader and bufio.Writer for efficient data transfer
  • Resource Cleanup: Automatically closes underlying TCP connections when handlers return
  • Error Handling: Returns errors for invalid frames, connection failures, and protocol violations

Security Features

  • SHA1 for Key Exchange: Uses SHA1 (required by RFC 6455) for computing the Sec-WebSocket-Accept header
  • Origin Validation: Supports both whitelist-based and custom validation functions
  • Wildcard Origins: Allows * for development but should be restricted in production
  • No Server Masking: Server messages are sent unmasked per protocol specification

Best Practices

  • Always validate origins in production
  • Handle disconnections gracefully
  • Use ping/pong for connection health
  • Clean up resources when connections close
  • Consider using a message broker for scaling

Testing

The middleware includes comprehensive test coverage for all major functionality:
Test CaseDescriptionExpected Behavior
TestIsWebSocketUpgradeValid WebSocket upgrade headersReturns true for valid upgrade requests
TestIsWebSocketUpgrade (case insensitive)Headers with mixed caseCorrectly identifies WebSocket upgrades regardless of case
TestIsWebSocketUpgrade (connection with keep-alive)Multiple connection valuesHandles Connection header with multiple values
TestIsWebSocketUpgrade (missing headers)Missing Upgrade or Connection headersReturns false when required headers are absent
TestComputeAcceptKeyRFC 6455 test vectorComputes correct Sec-WebSocket-Accept value
TestNew_NonWebSocketRegular HTTP requestPasses through to next handler without upgrade
TestWithOptions_ForbiddenOriginOrigin not in allowed listReturns 403 Forbidden
TestWithOptions_AllowedOriginOrigin in allowed listAccepts the connection
TestWithOptions_WildcardOriginWildcard (*) originAllows any origin
TestWithOptions_MissingKeyMissing Sec-WebSocket-Key headerReturns 400 Bad Request
TestConn_WriteTextWriting text messageSuccessfully writes text frame
TestConn_WriteBinaryWriting binary messageSuccessfully writes binary frame
TestConn_WriteMessageLengthsVarious payload sizes (10, 126, 200, 70000 bytes)Correctly encodes all payload length formats
TestConn_CloseConnection closeSends close frame and closes underlying connection
TestConn_PingPing messageSends ping frame with payload
TestConn_PongPong messageSends pong frame with payload
TestConn_ReadMessage (short unmasked text)Reading unmasked text frameCorrectly parses and returns message
TestConn_ReadMessage (masked text)Reading masked text frameUnmasks and returns correct data
TestConn_ReadMessageExtendedLength200-byte message (2-byte length)Correctly decodes 126+ byte payloads
TestConn_ReadMessageVeryLongLength70000-byte message (8-byte length)Correctly decodes 65536+ byte payloads
TestConn_ReadMessageReadErrorsIncomplete frame dataReturns appropriate errors
TestWithOptions_SubprotocolsMatching subprotocolAccepts connection with negotiated subprotocol
TestWithOptions_SubprotocolNotMatchingNon-matching subprotocolProceeds without subprotocol
TestWithOptions_CustomCheckOriginCustom origin validation functionCalls custom validator and respects result
TestWithOptions_NoOriginsAllowsAllNo origins configuredAllows all origins by default
TestWithOptions_OriginNotInListOrigin not in whitelistReturns 403 Forbidden
TestMessageConstantsMessage type constantsVerifies correct constant values per RFC 6455
TestErrorsError typesValidates error messages
  • sse - Server-Sent Events
  • timeout - Request timeout