Documentation
¶
Overview ¶
Package http provides utilities for HTTP request handling and production-grade WebSocket connections.
This package contains two main areas of functionality:
- HTTP utilities for simplified request/response handling
- 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 ¶
- Variables
- func CORS(next http.Handler) http.Handler
- func Call(req *http.Request, client *http.Client) (response any, err error)
- func ErrorToHttpCode(err error) int
- func HTTPErrorCode(err error) int
- func JsonGet(url string, onReq func(req *http.Request)) (any, *http.Response, error)
- func JsonToQueryString(json map[string]any) string
- func MakeUrl(host, path string, args string) (url string)
- func NewBytesRequest(method string, endpoint string, body []byte) (req *http.Request, err error)
- func NewJsonRequest(method string, endpoint string, body map[string]any) (req *http.Request, err error)
- func NewRequest(method string, endpoint string, bodyReader io.Reader) (req *http.Request, err error)
- func NormalizeWsUrl(httpOrWsUrl string) string
- func SendJsonResponse(writer http.ResponseWriter, resp any, err error)
- func WSConnJSONReaderWriter(conn *websocket.Conn) (reader *conc.Reader[gut.StrMap], writer *conc.Writer[conc.Message[gut.StrMap]])
- func WSConnWriteError(wsConn *websocket.Conn, err error) error
- func WSConnWriteMessage(wsConn *websocket.Conn, msg any) error
- func WSHandleConn[I any, S WSConn[I]](conn *websocket.Conn, ctx S, config *WSConnConfig)
- func WSServe[I any, S WSConn[I]](handler WSHandler[I, S], config *WSConnConfig) http.HandlerFunc
- type BiDirStreamConfig
- type BiDirStreamConn
- type HTTPError
- type JSONConn
- func (j *JSONConn) ConnId() string
- func (j *JSONConn) DebugInfo() any
- func (j *JSONConn) HandleMessage(msg any) error
- func (j *JSONConn) Name() string
- func (j *JSONConn) OnClose()
- func (j *JSONConn) OnError(err error) error
- func (j *JSONConn) OnStart(conn *websocket.Conn) error
- func (j *JSONConn) OnTimeout() bool
- func (j *JSONConn) ReadMessage(conn *websocket.Conn) (out any, err error)
- func (j *JSONConn) SendPing() error
- type JSONHandler
- type RequestVarMap
- type URLWaiter
- type WSConn
- type WSConnConfig
- type WSHandler
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var DefaultHttpClient *http.Client
var HighQPSHttpClient *http.Client
var LowQPSHttpClient *http.Client
var MediumQPSHttpClient *http.Client
Functions ¶
func Call ¶
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 HTTPErrorCode ¶
func JsonGet ¶
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 ¶
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 NewBytesRequest ¶
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 ¶
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 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 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) HandleMessage ¶
Called to handle the next message from the input stream on the ws conn.
func (*JSONConn) OnStart ¶
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 ¶
Handle read timeouts. By default returns true to disconnect and close on a timeout.
func (*JSONConn) ReadMessage ¶
Reads the next message from the websocket connection as a JSON payload
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
}
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.
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)