http

package
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2025 License: Apache-2.0 Imports: 17 Imported by: 6

Documentation

Overview

Package http provides utilities for HTTP request handling and production-grade WebSocket connections.

This package contains two main areas of functionality:

  1. HTTP utilities for simplified request/response handling
  2. WebSocket abstractions built on Gorilla WebSocket for real-time applications

WebSocket Framework

The WebSocket framework provides a production-ready abstraction over Gorilla WebSocket with automatic connection management, heartbeat detection, and lifecycle hooks.

## Key Features

  • Production-grade connection management with automatic heartbeat detection
  • Lifecycle hooks for connection start, close, timeout, and error handling
  • Thread-safe message broadcasting to multiple clients
  • Automatic ping-pong mechanism to prevent connection timeouts
  • Type-safe message handling with Go generics
  • Built-in JSON message support with JSONConn
  • Configurable timeouts and intervals for different deployment scenarios

## Quick Start

The simplest way to create a WebSocket endpoint is using the built-in JSONConn:

type EchoConn struct {
	gohttp.JSONConn
}

func (e *EchoConn) HandleMessage(msg any) error {
	// Echo the message back to the client
	e.Writer.Send(conc.Message[any]{Value: msg})
	return nil
}

type EchoHandler struct{}

func (h *EchoHandler) Validate(w http.ResponseWriter, r *http.Request) (*EchoConn, bool) {
	return &EchoConn{}, true // Accept all connections
}

// Register with HTTP router
router.HandleFunc("/echo", gohttp.WSServe(&EchoHandler{}, nil))

## Architecture

The framework uses three main interfaces:

### WSConn[I any] Represents a WebSocket connection that can handle typed messages:

type WSConn[I any] interface {
	BiDirStreamConn[I]
	ReadMessage(w *websocket.Conn) (I, error)
	OnStart(conn *websocket.Conn) error
}

### WSHandler[I any, S WSConn[I]] Validates HTTP requests and creates WebSocket connections:

type WSHandler[I any, S WSConn[I]] interface {
	Validate(w http.ResponseWriter, r *http.Request) (S, bool)
}

### BiDirStreamConn[I any] Provides lifecycle and message handling methods:

type BiDirStreamConn[I any] interface {
	SendPing() error
	Name() string
	ConnId() string
	HandleMessage(msg I) error
	OnError(err error) error
	OnClose()
	OnTimeout() bool
}

## Advanced Usage

### Multi-user Chat Server

type ChatServer struct {
	clients map[string]*ChatConn
	rooms   map[string]*Room
	mu      sync.RWMutex
}

type ChatConn struct {
	gohttp.JSONConn
	server   *ChatServer
	username string
	roomName string
}

func (c *ChatConn) OnStart(conn *websocket.Conn) error {
	if err := c.JSONConn.OnStart(conn); err != nil {
		return err
	}

	// Register with server
	c.server.mu.Lock()
	c.server.clients[c.ConnId()] = c
	c.server.mu.Unlock()

	return nil
}

func (c *ChatConn) HandleMessage(msg any) error {
	msgMap := msg.(map[string]any)

	switch msgMap["type"].(string) {
	case "chat":
		// Broadcast to all clients in room
		c.server.broadcastToRoom(c.roomName, msgMap)
	case "join_room":
		// Switch rooms
		c.joinRoom(msgMap["room"].(string))
	}

	return nil
}

### Authentication and Authorization

type SecureConn struct {
	gohttp.JSONConn
	userID string
	roles  []string
}

func (s *SecureConn) HandleMessage(msg any) error {
	msgMap := msg.(map[string]any)
	messageType := msgMap["type"].(string)

	if !s.hasPermission(messageType) {
		errorMsg := map[string]any{
			"type":  "error",
			"error": "Insufficient permissions",
		}
		s.Writer.Send(conc.Message[any]{Value: errorMsg})
		return nil
	}

	// Process authorized message
	return s.processMessage(msgMap)
}

type AuthHandler struct{}

func (h *AuthHandler) Validate(w http.ResponseWriter, r *http.Request) (*SecureConn, bool) {
	token := r.Header.Get("Authorization")

	// Validate JWT token
	claims, err := validateJWT(token)
	if err != nil {
		http.Error(w, "Invalid token", http.StatusUnauthorized)
		return nil, false
	}

	return &SecureConn{
		userID: claims["userID"].(string),
		roles:  claims["roles"].([]string),
	}, true
}

### Custom Ping-Pong with Latency Tracking

type MonitoredConn struct {
	gohttp.JSONConn
	lastPingTime time.Time
	pingCount    int64
}

func (m *MonitoredConn) SendPing() error {
	m.pingCount++
	m.lastPingTime = time.Now()

	pingMsg := map[string]any{
		"type":      "ping",
		"pingId":    m.pingCount,
		"timestamp": m.lastPingTime.Unix(),
	}

	m.Writer.Send(conc.Message[any]{Value: pingMsg})
	return nil
}

func (m *MonitoredConn) HandleMessage(msg any) error {
	msgMap := msg.(map[string]any)

	if msgMap["type"].(string) == "pong" {
		latency := time.Since(m.lastPingTime)
		log.Printf("Ping-pong latency: %v", latency)
	}

	return nil
}

## Configuration

### Production Configuration

config := &gohttp.WSConnConfig{
	BiDirStreamConfig: &gohttp.BiDirStreamConfig{
		PingPeriod: time.Second * 30,  // Send ping every 30 seconds
		PongPeriod: time.Second * 300, // Timeout after 5 minutes
	},
	Upgrader: websocket.Upgrader{
		ReadBufferSize:  4096,
		WriteBufferSize: 4096,
		CheckOrigin: func(r *http.Request) bool {
			// Implement proper origin checking
			return isValidOrigin(r.Header.Get("Origin"))
		},
	},
}

router.HandleFunc("/ws", gohttp.WSServe(handler, config))

### Development Configuration

config := gohttp.DefaultWSConnConfig()
config.PingPeriod = time.Second * 10  // Faster pings for development
config.PongPeriod = time.Second * 30  // Shorter timeout

## Frontend Integration

The framework is designed to work seamlessly with JavaScript WebSocket clients:

class WebSocketClient {
	constructor(url) {
		this.url = url;
		this.ws = null;
		this.pingInterval = null;
	}

	connect() {
		this.ws = new WebSocket(this.url);

		this.ws.onopen = () => {
			this.startPingInterval();
		};

		this.ws.onmessage = (event) => {
			const message = JSON.parse(event.data);
			this.handleMessage(message);
		};
	}

	handleMessage(message) {
		switch (message.type) {
			case 'ping':
				// Respond to server ping
				this.send({type: 'pong', pingId: message.pingId});
				break;
			default:
				this.onMessage(message);
		}
	}

	startPingInterval() {
		this.pingInterval = setInterval(() => {
			if (this.ws?.readyState === WebSocket.OPEN) {
				this.send({type: 'ping', timestamp: Date.now()});
			}
		}, 25000);
	}
}

## Error Handling and Resilience

The framework provides robust error handling:

type ResilientConn struct {
	gohttp.JSONConn
	errorCount int
	maxErrors  int
}

func (r *ResilientConn) OnError(err error) error {
	r.errorCount++
	log.Printf("WebSocket error #%d: %v", r.errorCount, err)

	if r.errorCount > r.maxErrors {
		return err // Close connection after too many errors
	}

	return nil // Continue with connection
}

func (r *ResilientConn) OnTimeout() bool {
	log.Printf("Connection timeout for %s", r.ConnId())
	return true // Close the connection
}

## Best Practices

  • Always call parent OnStart/OnClose when embedding JSONConn
  • Implement proper authentication in the Validate method
  • Use connection limits to prevent resource exhaustion
  • Handle errors gracefully without crashing the server
  • Clean up resources properly in OnClose methods
  • Use mutexes for thread-safe operations on shared state
  • Monitor connection health with metrics

## Common Patterns

### Hub Pattern for Broadcasting

type Hub struct {
	clients    map[*Client]bool
	broadcast  chan []byte
	register   chan *Client
	unregister chan *Client
}

func (h *Hub) run() {
	for {
		select {
		case client := <-h.register:
			h.clients[client] = true
		case client := <-h.unregister:
			delete(h.clients, client)
		case message := <-h.broadcast:
			for client := range h.clients {
				client.send <- message
			}
		}
	}
}

### Room-based Messaging

type Room struct {
	name    string
	clients map[string]*Client
	mu      sync.RWMutex
}

func (r *Room) Broadcast(message any, excludeId string) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	for id, client := range r.clients {
		if id != excludeId {
			client.Send(message)
		}
	}
}

## Testing

The package includes comprehensive test utilities for WebSocket testing:

// Create test server
server := httptest.NewServer(router)
defer server.Close()

// Create WebSocket client
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/endpoint"
conn, err := createTestClient(t, wsURL, nil)
if err != nil {
	t.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()

See the comprehensive test suite in ws2_test.go for complete examples of testing WebSocket applications including authentication, load testing, and error scenarios.

HTTP Utilities

The package also provides utilities for HTTP request/response handling:

  • JsonToQueryString: Convert maps to URL query strings
  • SendJsonResponse: Send JSON responses with proper error handling
  • ErrorToHttpCode: Convert Go errors to appropriate HTTP status codes
  • WSConnWriteMessage/WSConnWriteError: WebSocket message writing utilities
  • NormalizeWsUrl: Convert HTTP URLs to WebSocket URLs

Example HTTP utility usage:

func handleAPI(w http.ResponseWriter, r *http.Request) {
	data, err := processRequest(r)
	gohttp.SendJsonResponse(w, data, err)
}

This comprehensive framework enables building production-ready real-time applications with WebSocket communication, from simple echo servers to complex multi-user systems with authentication, rooms, and advanced connection management.

Example

Example demonstrating complete WebSocket workflow

// Create router and server
router := mux.NewRouter()

// Chat server setup
chatServer := &ChatServer{
	clients: make(map[string]*ChatConn),
	rooms:   make(map[string]*Room),
}

// Add handlers
router.HandleFunc("/echo", WSServe(&EchoHandler{}, nil))
router.HandleFunc("/chat", WSServe(&ChatHandler{server: chatServer}, nil))
router.HandleFunc("/secure", WSServe(&AuthHandler{}, nil))

// Custom configuration for production
config := &WSConnConfig{
	BiDirStreamConfig: &BiDirStreamConfig{
		PingPeriod: time.Second * 30,
		PongPeriod: time.Second * 300,
	},
	Upgrader: websocket.Upgrader{
		ReadBufferSize:  4096,
		WriteBufferSize: 4096,
		CheckOrigin: func(r *http.Request) bool {
			// Add proper origin checking in production
			return true
		},
	},
}

router.HandleFunc("/production", WSServe(&EchoHandler{}, config))

// In a real application, you would start the server:
// log.Fatal(http.ListenAndServe(":8080", router))

fmt.Println("WebSocket server configured with multiple endpoints:")
fmt.Println("- /echo: Simple echo server")
fmt.Println("- /chat: Multi-user chat with rooms")
fmt.Println("- /secure: Authenticated connections")
fmt.Println("- /production: Production-ready config")
Output:
WebSocket server configured with multiple endpoints:
- /echo: Simple echo server
- /chat: Multi-user chat with rooms
- /secure: Authenticated connections
- /production: Production-ready config

Index

Examples

Constants

This section is empty.

Variables

View Source
var DefaultHttpClient *http.Client
View Source
var HighQPSHttpClient *http.Client
View Source
var LowQPSHttpClient *http.Client
View Source
var MediumQPSHttpClient *http.Client

Functions

func CORS

func CORS(next http.Handler) http.Handler

A very sample http handler func that disables CORS for local development.

func Call

func Call(req *http.Request, client *http.Client) (response any, err error)

Makes a http with the tiven request and the http client. This is a wrapper over the standard library caller that creates a Client (if not provided), performs the request reads the entire body adn optionally converts the payload to an appropriate type based on the response' Content-Type header (for now only application/json is supported.

func ErrorToHttpCode

func ErrorToHttpCode(err error) int

func HTTPErrorCode

func HTTPErrorCode(err error) int

func JsonGet

func JsonGet(url string, onReq func(req *http.Request)) (any, *http.Response, error)

A simple wrapper for performing JSON Get requests. The url is the full url once all query params have been added. The onReq callback allows customization of the http requests before it is sent.

func JsonToQueryString

func JsonToQueryString(json map[string]any) string

Helper method to convert a map into a json query string

Example
input := map[string]any{"a": 1, "b": 2}
queryStr := JsonToQueryString(input)
fmt.Println(queryStr)
Output:
a=1&b=2

func MakeUrl

func MakeUrl(host, path string, args string) (url string)

Creates a URL on a host, path and with optional query parameters

func NewBytesRequest

func NewBytesRequest(method string, endpoint string, body []byte) (req *http.Request, err error)

Wraps the NewRequest helper to create request to set the body from a byte array.

func NewJsonRequest

func NewJsonRequest(method string, endpoint string, body map[string]any) (req *http.Request, err error)

Wraps the NewRequest helper to create a request with the payload marshalled as JSON.

func NewRequest

func NewRequest(method string, endpoint string, bodyReader io.Reader) (req *http.Request, err error)

Creates a new http request with the given method, endpoint and a bodyready that provides the content the request body.

func NormalizeWsUrl

func NormalizeWsUrl(httpOrWsUrl string) string

Returns a normalized WS url equivalent for a given http url.

Example
fmt.Println(NormalizeWsUrl("http://google.com"))
fmt.Println(NormalizeWsUrl("https://github.com"))
Output:
ws://google.com
wss://github.com

func SendJsonResponse

func SendJsonResponse(writer http.ResponseWriter, resp any, err error)

func WSConnJSONReaderWriter

func WSConnJSONReaderWriter(conn *websocket.Conn) (reader *conc.Reader[gut.StrMap], writer *conc.Writer[conc.Message[gut.StrMap]])

func WSConnWriteError

func WSConnWriteError(wsConn *websocket.Conn, err error) error

func WSConnWriteMessage

func WSConnWriteMessage(wsConn *websocket.Conn, msg any) error

func WSHandleConn

func WSHandleConn[I any, S WSConn[I]](conn *websocket.Conn, ctx S, config *WSConnConfig)

Once a websocket connection is established (either by the server or by the client), this method handles the lifecycle of the connection by taking care of (healthceck) pings, handling closures, handling received messages.

Example
{
}

func WSServe

func WSServe[I any, S WSConn[I]](handler WSHandler[I, S], config *WSConnConfig) http.HandlerFunc

Returns a http.HandlerFunc that takes care of upgrading the request to a Websocket connection and handling its lifecycle by delegating important activities to the WSHandler type This method is often used to create a handler for particular routes on http routers.

The handler parameter is responsible for validating (eg authenticating/authorizing) the request to ensure an upgrade is allowed as well as handling messages received on the upgraded connection.

Example
r := mux.NewRouter()
r.HandleFunc("/publish", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Publishing Custom Message")
})

r.HandleFunc("/subscribe", WSServe(&JSONHandler{}, nil))
srv := http.Server{Handler: r}
log.Fatal(srv.ListenAndServe())

Types

type BiDirStreamConfig

type BiDirStreamConfig struct {
	// Our connection can send pings at repeated intervals as a form of
	// healthcheck.  This property is a way to specify that duration.
	PingPeriod time.Duration

	// If no data (ping or otherwise) has been received from the remote
	// side within this duration then it is an indication for the handler to treat
	// this as a closed/timedout connection.  The handler an chose to terminate the connection
	// at this point by handling the OnTimeout method on the Conn interface.
	PongPeriod time.Duration
}

Configuration for a bidirectional "stream"

func DefaultBiDirStreamConfig

func DefaultBiDirStreamConfig() *BiDirStreamConfig

Creates a bidirectional stream config with default values for ping and pong durations.

type BiDirStreamConn

type BiDirStreamConn[I any] interface {
	// Called to send the next ping message.
	SendPing() error

	// Optional Name of the connection
	Name() string

	// Optional connection id
	ConnId() string

	// Called to handle the next message from the input stream on the ws conn.
	HandleMessage(msg I) error

	// Called to handle or suppress an error
	OnError(err error) error

	// Called when the connection closes.
	OnClose()

	// Called when data has not been received within the PongPeriod.
	OnTimeout() bool
}

type HTTPError

type HTTPError struct {
	Code    int
	Message string
}

Representing HTTP specific errors

func (*HTTPError) Error

func (t *HTTPError) Error() string

type JSONConn

type JSONConn struct {
	// The output writer as a channel to send outgoing messages on
	Writer *conc.Writer[conc.Message[any]]

	// Name of this connection (for clarity)
	NameStr string

	// A connection ID to identify this connection
	ConnIdStr string

	// Keeps track of the current Ping count to send with the ping
	PingId int64
}

An implementation of the WSConn interface that exchanges JSON message paylods

func (*JSONConn) ConnId

func (j *JSONConn) ConnId() string

Returns the (possibly auto-generated) Connection Id

func (*JSONConn) DebugInfo

func (j *JSONConn) DebugInfo() any

Basic debug information.

func (*JSONConn) HandleMessage

func (j *JSONConn) HandleMessage(msg any) error

Called to handle the next message from the input stream on the ws conn.

func (*JSONConn) Name

func (j *JSONConn) Name() string

Gets the name of this connection

func (*JSONConn) OnClose

func (j *JSONConn) OnClose()

Called when the connection closes.

func (*JSONConn) OnError

func (j *JSONConn) OnError(err error) error

func (*JSONConn) OnStart

func (j *JSONConn) OnStart(conn *websocket.Conn) error

This (callback) method is called when the websocket connection is upgraded but before the websocket event loop begins (in the WSHandleConn method)

func (*JSONConn) OnTimeout

func (j *JSONConn) OnTimeout() bool

Handle read timeouts. By default returns true to disconnect and close on a timeout.

func (*JSONConn) ReadMessage

func (j *JSONConn) ReadMessage(conn *websocket.Conn) (out any, err error)

Reads the next message from the websocket connection as a JSON payload

func (*JSONConn) SendPing

func (j *JSONConn) SendPing() error

Called to send the next ping message.

type JSONHandler

type JSONHandler struct {
}

func (*JSONHandler) Validate

func (j *JSONHandler) Validate(w http.ResponseWriter, r *http.Request) (out WSConn[any], isValid bool)

type RequestVarMap

type RequestVarMap struct {
	// contains filtered or unexported fields
}

func (*RequestVarMap) GetKey

func (r *RequestVarMap) GetKey(req *http.Request, key string) (value any)

func (*RequestVarMap) SetKey

func (r *RequestVarMap) SetKey(req *http.Request, key string, value any)

type URLWaiter

type URLWaiter struct {
	Method             string
	Url                string
	Headers            map[string]string
	Payload            map[string]any
	DelayBetweenChecks time.Duration

	// Func versions of above so we can do something dynamcially on each iteration
	RequestFunc  func(iter int, prevError error) (*http.Request, error)
	ValidateFunc func(req *http.Request, resp *http.Response) error
}

A simple utility that waits for a url to return a successful response before proceeding. This can be used for things like waiting for a database or another service to become available before performing other activities.

func (*URLWaiter) Run

func (u *URLWaiter) Run() (success bool, iter int, err error)

Runs the "waiter".

Returns a tuple of:

	success - whether the waited-upon eventually became active.
	iter    - How many iterations where run before a success/failure
 	err     - Error encountered if the "wait" was a failure

type WSConn

type WSConn[I any] interface {
	BiDirStreamConn[I]

	// Reads the next message from the ws conn.
	ReadMessage(w *websocket.Conn) (I, error)

	// Callback to be called when the WS connection is started
	OnStart(conn *websocket.Conn) error
}

Represents a bidirectional websocket connection

type WSConnConfig

type WSConnConfig struct {
	*BiDirStreamConfig
	Upgrader websocket.Upgrader
}

Extends BiDirStreamConfig to include Websocket specific configrations

func DefaultWSConnConfig

func DefaultWSConnConfig() *WSConnConfig

This method creates a WSConnConfig with a default websocket Upgrader

type WSHandler

type WSHandler[I any, S WSConn[I]] interface {
	// Called to validate an http request to see if it is upgradeable to a ws conn
	Validate(w http.ResponseWriter, r *http.Request) (S, bool)
}

Handlers validate a http request and decide whether they can/should be upgraded to create and begin a websocket connection (WSConn)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL