GitHunt
JK

jkaninda/okapiws

Okapi WebSocket is a lightweight, framework-agnostic WebSocket package focused on developer experience and clean APIs

Okapi WebSocket

Okapi WebSocket is a lightweight, framework-agnostic WebSocket package for Go, providing both server and client with clean, callback-driven APIs.
It integrates seamlessly with the Okapi Web Framework, but works with Go's standard net/http or any HTTP server.

Features

Server

  • Simple WebSocket upgrade API (Default or NewWSUpgrader)
  • Framework independent — works with net/http, Okapi, or any Go HTTP server
  • Optional configuration (nil uses sensible defaults)
  • OnMessage / OnError / OnClose callback handlers
  • Send, SendText, SendJSON, SendEvent, SendBinary methods
  • Text and binary message support
  • Custom response headers on upgrade
  • Context-based lifecycle (inherits request context)
  • Graceful connection closing with close frames
  • Automatic ping/pong keep-alive
  • Configurable buffer sizes, timeouts, max message size, CORS, subprotocols, and compression

Client

  • Connect to any WebSocket server (ws:// or wss://)
  • Same callback-driven API: OnMessage / OnError / OnClose / OnConnect
  • Same send methods: Send, SendText, SendJSON, SendEvent, SendBinary
  • Auto-reconnect with exponential backoff and jitter
  • Configurable max retries, initial delay, and max delay
  • Custom HTTP headers for handshake
  • Custom TLS configuration
  • Context-based lifecycle and graceful shutdown
  • Thread-safe

Okapi

Installation

go get github.com/jkaninda/okapiws

Server

Using with Go net/http

package main

import (
	"log"
	"net/http"

	okapiws "github.com/jkaninda/okapiws"
)

func main() {
	http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
		upgrader := okapiws.NewWSUpgrader(nil) // nil = use default config

		ws, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			http.Error(w, "WebSocket upgrade failed", http.StatusBadRequest)
			return
		}
		defer ws.Close()

		ws.OnMessage(func(msg *okapiws.WSMessage) {
			log.Printf("[%d] %s", msg.Type, msg.Data)
			_ = ws.Send(msg.Data) // Echo
		})

		ws.OnError(func(err error) {
			log.Printf("WebSocket error: %v", err)
		})

		ws.Start()

		<-ws.Context().Done() // Block until closed
	})

	log.Println("Listening on :8080...")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Using with Okapi

package main

import (
	"log"
	"net/http"

	"github.com/jkaninda/okapi"
	okapiws "github.com/jkaninda/okapiws"
)

// WebSocket upgrades the HTTP connection to WebSocket.
// Config is optional; pass nil to use default settings.
func WebSocket(config *okapiws.WSConfig, c *okapi.Context) (*okapiws.WSConnection, error) {
	upgrader := okapiws.NewWSUpgrader(config)
	return upgrader.Upgrade(c.Response(), c.Request(), nil)
}

// WebSocketWithHeaders upgrades with additional response headers.
func WebSocketWithHeaders(config *okapiws.WSConfig, headers http.Header, c okapi.Context) (*okapiws.WSConnection, error) {
	upgrader := okapiws.NewWSUpgrader(config)
	return upgrader.Upgrade(c.Response(), c.Request(), headers)
}

func main() {
	app := okapi.Default()

	app.Get("/", func(c *okapi.Context) error {
		return c.OK(okapi.M{"message": "Hello from Okapi Web Framework!"})
	})

	app.Get("/ws", handleWebSocket)

	if err := app.Start(); err != nil {
		panic(err)
	}
}

func handleWebSocket(c *okapi.Context) error {
	ws, err := WebSocket(nil, c)
	if err != nil {
		return err
	}
	defer func() {
		if err := ws.Close(); err != nil {
			log.Printf("error closing WebSocket: %v", err)
		}
	}()

	ws.OnMessage(func(msg *okapiws.WSMessage) {
		log.Printf("[%d] %s", msg.Type, msg.Data)
		// Echo the message back
		_ = ws.Send(msg.Data)
	})

	ws.OnError(func(err error) {
		log.Printf("WebSocket error: %v", err)
	})

	ws.Start()

	// Block until the connection is closed
	<-ws.Context().Done()
	return nil
}

Custom Response Headers

headers := http.Header{}
headers.Add("X-WebSocket-Version", "1.0")

ws, err := upgrader.Upgrade(w, r, headers)

Sending Messages

// Text
ws.Send([]byte("hello"))
ws.SendText("hello")

// JSON
ws.SendJSON(map[string]any{"key": "value"})

// Event (custom protocol with {"event": "...", "data": ...})
ws.SendEvent("chat", map[string]any{"message": "hi"})

// Binary
ws.SendBinary(binaryData)

Client

Basic Usage

package main

import (
	"context"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	okapiws "github.com/jkaninda/okapiws"
)

func main() {
	config := okapiws.DefaultWSClient()
	config.AutoReconnect = true
	config.ReconnectInitial = 1 * time.Second
	config.ReconnectMax = 30 * time.Second
	config.MaxRetries = 10

	client := okapiws.NewWSClient("ws://localhost:8080/ws", okapiws.WithConfig(config))

	client.OnConnect(func() {
		log.Println("Connected to server")
		_ = client.SendText("Hello from client!")
	})

	client.OnMessage(func(msg *okapiws.WSMessage) {
		log.Printf("Received [%d]: %s", msg.Type, msg.Data)
	})

	client.OnError(func(err error) {
		log.Printf("Error: %v", err)
	})

	client.OnClose(func() {
		log.Println("Connection closed")
	})

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	if err := client.Connect(ctx); err != nil {
		log.Fatalf("Failed to connect: %v", err)
	}

	// Send a message every 5 seconds
	go func() {
		ticker := time.NewTicker(5 * time.Second)
		defer ticker.Stop()
		for {
			select {
			case <-ticker.C:
				if err := client.SendText("ping from client"); err != nil {
					log.Printf("Send error: %v", err)
				}
			case <-ctx.Done():
				return
			}
		}
	}()

	// Wait for interrupt signal
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
	<-sigCh

	log.Println("Shutting down...")
	cancel()
	if err := client.Close(); err != nil {
		log.Printf("Close error: %v", err)
	}
}

Custom Headers and TLS

config := okapiws.DefaultWSClient()
config.Headers = http.Header{
	"Authorization": []string{"Bearer my-token"},
}
config.TLSConfig = &tls.Config{
	InsecureSkipVerify: true,
}

client := okapiws.NewWSClient("wss://example.com/ws", okapiws.WithConfig(config))

Auto-Reconnect

When AutoReconnect is enabled, the client automatically reconnects on connection loss using exponential backoff with jitter. The OnConnect callback fires on each successful reconnection.

config := okapiws.DefaultWSClient()
config.AutoReconnect = true
config.ReconnectInitial = 1 * time.Second  // first retry after 1s
config.ReconnectMax = 30 * time.Second     // cap backoff at 30s
config.MaxRetries = 0                       // 0 = unlimited retries

Configuration

Server Configuration (WSConfig)

Field Default Description
ReadBufferSize 1024 Read buffer size in bytes
WriteBufferSize 1024 Write buffer size in bytes
HandshakeTimeout 10s Handshake timeout
CheckOrigin true Origin check function
Subprotocols nil Supported subprotocols
EnableCompression false Enable per-message compression
PingInterval 54s Interval between pings
PongWait 60s Timeout waiting for pong
WriteWait 10s Write deadline timeout
MaxMessageSize 512KB Maximum incoming message size
cfg := &okapiws.WSConfig{
	ReadBufferSize:  2048,
	WriteBufferSize: 2048,
	MaxMessageSize:  1024 * 1024, // 1MB
}
upgrader := okapiws.NewWSUpgrader(cfg)

Pass nil to use defaults:

ws, err := okapiws.Default(w, r, nil)

Client Configuration (WSClientConfig)

Field Default Description
ReadBufferSize 1024 Read buffer size in bytes
WriteBufferSize 1024 Write buffer size in bytes
HandshakeTimeout 10s Handshake timeout
Headers nil Custom HTTP headers for handshake
TLSConfig nil Custom TLS configuration
Subprotocols nil Requested subprotocols
EnableCompression false Enable per-message compression
PingInterval 54s Interval between pings
PongWait 60s Timeout waiting for pong
WriteWait 10s Write deadline timeout
MaxMessageSize 512KB Maximum incoming message size
AutoReconnect false Enable auto-reconnect on disconnect
ReconnectInitial 1s Initial reconnect delay
ReconnectMax 30s Maximum reconnect delay
MaxRetries 0 Max reconnect attempts (0 = unlimited)

License

This project is licensed under the MIT License. See the LICENSE file for details.

jkaninda/okapiws | GitHunt