Files
golib/httpserver
nabbar 8fabc2f5e1 Package HTTPServer:
- Add: error for invalid address check
- Move: errors string InvalidInstance as golib/error type
- Update: add check of listen address in port use function
2025-12-22 13:08:08 +01:00
..
2025-12-22 13:08:08 +01:00
2025-12-22 13:08:08 +01:00
2023-11-21 20:21:37 +01:00

HTTP Server Package

License: MIT Go Version Tests Coverage

Production-grade HTTP server management for Go with lifecycle control, TLS support, pool orchestration, and integrated monitoring.


Table of Contents


Overview

The httpserver package provides a robust abstraction layer for managing HTTP/HTTPS servers in Go applications. It emphasizes production readiness with comprehensive lifecycle management, configuration validation, TLS support, and the ability to orchestrate multiple servers through a unified pool interface.

Design Philosophy

  1. Lifecycle Management: Full control over server start, stop, and restart operations
  2. Configuration-Driven: Declarative configuration with validation
  3. Thread-Safe: Atomic operations and proper synchronization for concurrent use
  4. Production-Ready: Monitoring, logging, and error handling built-in
  5. Composable: Pool management for coordinating multiple server instances

Key Features

  • Complete Lifecycle Control: Start, stop, restart servers with context-aware operations
  • Configuration Validation: Built-in validation with detailed error reporting
  • TLS/HTTPS Support: Integrated certificate management with optional/mandatory modes
  • Pool Management: Coordinate multiple servers with unified operations and filtering
  • Handler Management: Dynamic handler registration with key-based routing
  • Monitoring Integration: Built-in health checks and metrics collection
  • Thread-Safe Operations: Atomic values and mutex protection for concurrent access
  • Port Conflict Detection: Automatic port availability checking before binding
  • Graceful Shutdown: Context-aware shutdown with configurable timeouts

Installation

go get github.com/nabbar/golib/httpserver

Architecture

Package Structure

The package is organized into three main components:

httpserver/
├── httpserver           # Core server implementation
│   ├── config.go        # Configuration and validation
│   ├── server.go        # Server lifecycle management
│   ├── run.go           # Start/stop execution logic
│   ├── handler.go       # Handler registration
│   ├── monitor.go       # Monitoring integration
│   └── interface.go     # Public interfaces
├── pool/                # Multi-server orchestration
│   ├── interface.go     # Pool interfaces
│   ├── server.go        # Pool operations
│   ├── list.go          # Filtering and listing
│   └── config.go        # Pool configuration
└── types/               # Shared type definitions
    ├── handler.go       # Handler types
    ├── fields.go        # Field type constants
    └── const.go         # Package constants

Component Diagram

┌─────────────────────────────────────────────────────┐
│                  Application Layer                   │
│           (Your HTTP Handlers & Routes)              │
└──────────────────┬──────────────────────────────────┘
                   │
         ┌─────────▼─────────┐
         │   httpserver      │
         │   Package API     │
         └─────────┬─────────┘
                   │
    ┌──────────────┼──────────────┐
    │              │              │
┌───▼───┐    ┌────▼────┐    ┌───▼────┐
│Server │    │  Pool   │    │ Types  │
│       │    │         │    │        │
│Config │◄───┤ Manager │    │Handler │
│Run    │    │ Filter  │    │Fields  │
│Monitor│    │ Clone   │    │Const   │
└───┬───┘    └────┬────┘    └────────┘
    │             │
    └──────┬──────┘
           │
    ┌──────▼──────┐
    │  Go stdlib  │
    │ http.Server │
    └─────────────┘

Thread Safety Architecture

Component Mechanism Concurrency Model
Server State atomic.Value Lock-free reads, atomic writes
Pool Map sync.RWMutex Multiple readers, exclusive writers
Handler Registry atomic.Value Lock-free handler swapping
Logger atomic.Value Thread-safe logging
Runner atomic.Value + sync.WaitGroup Lifecycle synchronization

Performance

Server Operations

Operation Time Memory Notes
Config Validation ~100ns O(1) Field validation only
Server Creation <1ms ~5KB Includes initialization
Start Server 1-5ms ~10KB Port binding overhead
Stop Server <5s O(1) Graceful shutdown timeout
Pool Operations O(n) ~1KB/server Linear scaling

Throughput

  • HTTP Requests: Limited by Go's http.Server (typically 50k+ req/s)
  • HTTPS/TLS: ~20-30k req/s depending on cipher suite
  • Pool Management: Negligible overhead (<1% per server)

Memory Usage

  • Single Server: ~10-15KB baseline + handler memory
  • Pool with 10 Servers: ~150KB baseline
  • Scale: Linear growth with server count

Use Cases

This package is designed for applications requiring robust HTTP server management:

Microservices Architecture

  • Run multiple API versions simultaneously (v1, v2, v3)
  • Separate admin and public endpoints on different ports
  • Blue-green deployments with gradual traffic shifting

Multi-Tenant Systems

  • Dedicated server per tenant with isolated configuration
  • Different TLS certificates per customer domain
  • Per-tenant rate limiting and monitoring

Development & Testing

  • Start/stop servers dynamically in integration tests
  • Multiple test environments on different ports
  • Mock servers with configurable behavior

API Gateways

  • Route traffic to multiple backend servers
  • Health checking and automatic failover
  • Centralized monitoring and logging

Production Deployments

  • Graceful shutdown during rolling updates
  • TLS certificate rotation without downtime
  • Structured logging and monitoring integration

Quick Start

Single Server

package main

import (
    "context"
    "net/http"
    "github.com/nabbar/golib/httpserver"
)

func main() {
    // Create server configuration
    cfg := httpserver.Config{
        Name:   "api-server",
        Listen: "127.0.0.1:8080",
        Expose: "http://localhost:8080",
    }
    
    // Register handler (required)
    cfg.RegisterHandlerFunc(func() map[string]http.Handler {
        mux := http.NewServeMux()
        mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(http.StatusOK)
            w.Write([]byte("OK"))
        })
        return map[string]http.Handler{
            "": mux, // Default handler
        }
    })
    
    // Validate configuration
    if err := cfg.Validate(); err != nil {
        panic(err)
    }
    
    // Create and start server
    srv, err := httpserver.New(cfg, nil)
    if err != nil {
        panic(err)
    }
    
    ctx := context.Background()
    if err := srv.Start(ctx); err != nil {
        panic(err)
    }
    
    // Server is now running...
    
    // Graceful shutdown
    defer srv.Stop(ctx)
}

Server Pool

package main

import (
    "context"
    "net/http"
    "github.com/nabbar/golib/httpserver"
    "github.com/nabbar/golib/httpserver/pool"
)

func main() {
    // Create handler function
    handlerFunc := func() map[string]http.Handler {
        mux := http.NewServeMux()
        mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("Hello from pool!"))
        })
        return map[string]http.Handler{"": mux}
    }
    
    // Create pool with handler
    p := pool.New(nil, handlerFunc)
    
    // Add multiple servers
    configs := []httpserver.Config{
        {Name: "api-v1", Listen: "127.0.0.1:8080", Expose: "http://localhost:8080"},
        {Name: "api-v2", Listen: "127.0.0.1:8081", Expose: "http://localhost:8081"},
        {Name: "admin", Listen: "127.0.0.1:8082", Expose: "http://localhost:8082"},
    }
    
    for _, cfg := range configs {
        if err := p.StoreNew(cfg, nil); err != nil {
            panic(err)
        }
    }
    
    // Start all servers
    ctx := context.Background()
    if err := p.Start(ctx); err != nil {
        panic(err)
    }
    
    // All servers running...
    
    // Stop all servers gracefully
    defer p.Stop(ctx)
}

Core Package: httpserver

The core package provides the foundational server abstraction with configuration, lifecycle management, and monitoring.

Configuration

The Config struct defines all server parameters with validation:

type Config struct {
    // Name identifies the server instance (required)
    Name string `validate:"required"`
    
    // Listen is the bind address - format: "ip:port" or "host:port" (required)
    // Examples: "127.0.0.1:8080", "0.0.0.0:443", "localhost:3000"
    Listen string `validate:"required,hostname_port"`
    
    // Expose is the public-facing URL for this server (required)
    // Used for generating URLs, monitoring, and service discovery
    // Examples: "http://localhost:8080", "https://api.example.com"
    Expose string `validate:"required,url"`
    
    // HandlerKey associates this server with a specific handler from the handler map
    // Allows multiple servers to use different handlers from a shared registry
    HandlerKey string
    
    // Disabled allows disabling a server without removing its configuration
    // Useful for maintenance mode or gradual rollout
    Disabled bool
    
    // Monitor configuration for health checks and metrics
    Monitor moncfg.Config
    
    // TLSMandatory requires valid TLS configuration to start the server
    // If true, server will fail to start without proper TLS setup
    TLSMandatory bool
    
    // TLS certificate configuration (optional)
    // If InheritDefault is true, uses default TLS config
    TLS libtls.Config
    
    // Additional HTTP/2 and timeout configuration...
}

Configuration Methods:

// Validate performs comprehensive validation on all fields
func (c Config) Validate() error

// Clone creates a deep copy of the configuration
func (c Config) Clone() Config

// RegisterHandlerFunc sets the handler function for this server
func (c *Config) RegisterHandlerFunc(f FuncHandler)

// SetDefaultTLS sets the default TLS configuration provider
func (c *Config) SetDefaultTLS(f FctTLSDefault)

// SetContext sets the parent context provider
func (c *Config) SetContext(f FuncContext)

// Server creates a new server instance from this configuration
func (c Config) Server(defLog FuncLog) (Server, error)

Server Interface

The Server interface provides full lifecycle and configuration control:

type Server interface {
    // Lifecycle Management
    Start(ctx context.Context) error     // Start the HTTP server
    Stop(ctx context.Context) error      // Gracefully stop the server
    Restart(ctx context.Context) error   // Stop then start the server
    IsRunning() bool                     // Check if server is running
    Uptime() time.Duration               // Get server uptime
    
    // Server Information
    GetName() string                     // Get server name
    GetBindable() string                 // Get bind address (Listen)
    GetExpose() string                   // Get expose URL
    IsDisable() bool                     // Check if server is disabled
    IsTLS() bool                         // Check if TLS is configured
    
    // Configuration Management
    GetConfig() *Config                  // Get current configuration
    SetConfig(cfg Config, defLog FuncLog) error  // Update configuration
    
    // Handler Management
    Handler(h FuncHandler)               // Set handler function
    Merge(s Server, def FuncLog) error   // Merge another server's config
    
    // Monitoring
    Monitor(vrs Version) (Monitor, error)  // Get monitoring data
    MonitorName() string                   // Get monitor identifier
}

Usage Examples

Basic HTTP Server

package main

import (
    "context"
    "net/http"
    "github.com/nabbar/golib/httpserver"
)

func main() {
    // Configure server
    cfg := httpserver.Config{
        Name:   "web-server",
        Listen: "0.0.0.0:8080",
        Expose: "http://api.example.com",
    }

    // Register HTTP handler
    cfg.RegisterHandlerFunc(func() map[string]http.Handler {
        mux := http.NewServeMux()
        mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("Hello World"))
        })
        mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
            w.WriteHeader(http.StatusOK)
        })
        return map[string]http.Handler{"": mux}
    })

    // Create and start server
    srv, _ := httpserver.New(cfg, nil)
    srv.Start(context.Background())
}

HTTPS Server with TLS

package main

import (
    "context"
    "github.com/nabbar/golib/certificates"
    "github.com/nabbar/golib/httpserver"
)

func main() {
    cfg := httpserver.Config{
        Name:         "secure-server",
        Listen:       "0.0.0.0:8443",
        Expose:       "https://secure.example.com",
        TLSMandatory: true,
        TLS: certificates.Config{
            CertPEM: "/path/to/cert.pem",
            KeyPEM:  "/path/to/key.pem",
            // Additional TLS configuration...
        },
    }

    cfg.RegisterHandlerFunc(func() map[string]http.Handler {
        // Your HTTPS handler
        return map[string]http.Handler{"": yourHandler}
    })

    srv, _ := httpserver.New(cfg, nil)
    srv.Start(context.Background())
}

Multiple Handlers with Keys

package main

import (
    "context"
    "net/http"
    "github.com/nabbar/golib/httpserver"
)

func main() {
    // Create handler registry
    handlerFunc := func() map[string]http.Handler {
        return map[string]http.Handler{
            "api-v1":  createAPIv1Handler(),
            "api-v2":  createAPIv2Handler(),
            "admin":   createAdminHandler(),
            "default": createDefaultHandler(),
        }
    }

    // Server using api-v1 handler
    cfg := httpserver.Config{
        Name:       "api-v1-server",
        Listen:     "127.0.0.1:8080",
        Expose:     "http://localhost:8080",
        HandlerKey: "api-v1",  // Select specific handler
    }
    cfg.RegisterHandlerFunc(handlerFunc)

    srv, _ := httpserver.New(cfg, nil)
    srv.Start(context.Background())
}

Disabled Server (Maintenance Mode)

cfg := httpserver.Config{
    Name:     "maintenance-server",
    Listen:   "127.0.0.1:8080",
    Expose:   "http://localhost:8080",
    Disabled: true,  // Server won't start, but config is preserved
}

srv, _ := httpserver.New(cfg, nil)
// Server will not start due to Disabled flag
srv.Start(context.Background())  // Returns immediately without error

Dynamic Restart with New Configuration

package main

import (
    "context"
    "github.com/nabbar/golib/httpserver"
)

func main() {
    // Initial configuration
    cfg1 := httpserver.Config{
        Name:   "dynamic-server",
        Listen: "127.0.0.1:8080",
        Expose: "http://localhost:8080",
    }
    cfg1.RegisterHandlerFunc(handlerFunc)

    srv, _ := httpserver.New(cfg1, nil)
    srv.Start(context.Background())

    // Later: update configuration (e.g., enable TLS)
    cfg2 := cfg1.Clone()
    cfg2.TLSMandatory = true
    cfg2.TLS = newTLSConfig
    cfg2.Expose = "https://localhost:8443"

    // Update and restart
    srv.SetConfig(cfg2, nil)
    srv.Restart(context.Background())
}

Subpackages

pool Subpackage

Multi-server orchestration with unified lifecycle management and advanced filtering capabilities.

Purpose: Coordinate multiple HTTP servers as a single unit with shared handlers, monitoring, and control operations.

Features

  • Unified Lifecycle: Start, stop, restart all servers with a single call
  • Dynamic Management: Add/remove servers at runtime
  • Advanced Filtering: Query servers by name, bind address, or expose URL
  • Pattern Matching: Support for glob patterns and regex filtering
  • Pool Operations: Clone, merge, and walk through server collections
  • Shared Handlers: Register handlers once for all servers
  • Aggregated Monitoring: Collect metrics from all servers
  • Thread-Safe: RWMutex protection for concurrent access

Pool Interface

type Pool interface {
    // Lifecycle Management (inherited from libsrv.Server)
    Start(ctx context.Context) error
    Stop(ctx context.Context) error
    Restart(ctx context.Context) error
    IsRunning() bool
    Uptime() time.Duration
    
    // Management Operations
    Walk(fct FuncWalk) bool                          // Iterate over all servers
    WalkLimit(fct FuncWalk, onlyBindAddress ...string) bool  // Iterate over specific servers
    Clean()                                          // Remove all servers
    Load(bindAddress string) Server                  // Get server by bind address
    Store(srv Server)                                // Add/update server
    Delete(bindAddress string)                       // Remove server
    StoreNew(cfg Config, defLog FuncLog) error      // Add new server from config
    LoadAndDelete(bindAddress string) (Server, bool)  // Atomic load-and-delete
    MonitorNames() []string                          // List all monitor names
    
    // Filtering Operations
    Has(bindAddress string) bool                     // Check if server exists
    Len() int                                        // Get server count
    List(fieldFilter, fieldReturn FieldType, pattern, regex string) []string
    Filter(field FieldType, pattern, regex string) Pool  // Create filtered view
    
    // Advanced Operations
    Clone(ctx context.Context) Pool                  // Deep copy pool
    Merge(p Pool, def FuncLog) error                // Merge another pool
    Handler(fct FuncHandler)                        // Set global handler
    Monitor(vrs Version) ([]Monitor, error)         // Get all monitors
}

Config Type

Pool configuration as a slice of server configs:

type Config []httpserver.Config

// Set global handler for all servers
func (p Config) SetHandlerFunc(hdl FuncHandler)

// Set global TLS configuration
func (p Config) SetDefaultTLS(f FctTLSDefault)

// Set global context provider
func (p Config) SetContext(f FuncContext)

// Validate all configurations
func (p Config) Validate() error

// Create pool from configurations
func (p Config) Pool(ctx FuncContext, hdl FuncHandler, defLog FuncLog) (Pool, error)

// Iterate over configurations
func (p Config) Walk(fct FuncWalkConfig)

Usage Examples

Basic Pool Management:

package main

import (
    "context"
    "net/http"
    "github.com/nabbar/golib/httpserver"
    "github.com/nabbar/golib/httpserver/pool"
)

func main() {
    // Create shared handler
    handler := func() map[string]http.Handler {
        mux := http.NewServeMux()
        mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            w.Write([]byte("Hello from pool"))
        })
        return map[string]http.Handler{"": mux}
    }

    // Create pool
    p := pool.New(nil, handler)

    // Add servers dynamically
    servers := []httpserver.Config{
        {Name: "api-v1", Listen: "127.0.0.1:8080", Expose: "http://localhost:8080"},
        {Name: "api-v2", Listen: "127.0.0.1:8081", Expose: "http://localhost:8081"},
        {Name: "admin", Listen: "127.0.0.1:9000", Expose: "http://localhost:9000"},
    }

    for _, cfg := range servers {
        if err := p.StoreNew(cfg, nil); err != nil {
            panic(err)
        }
    }

    // Start all servers
    ctx := context.Background()
    if err := p.Start(ctx); err != nil {
        panic(err)
    }

    // Check status
    println("Running servers:", p.Len())
    println("All running:", p.IsRunning())

    // Stop all
    defer p.Stop(ctx)
}

Pool from Configuration:

package main

import (
    "context"
    "github.com/nabbar/golib/httpserver"
    "github.com/nabbar/golib/httpserver/pool"
)

func main() {
    // Define configurations
    configs := pool.Config{
        httpserver.Config{
            Name:   "web-frontend",
            Listen: "0.0.0.0:8080",
            Expose: "http://example.com",
        },
        httpserver.Config{
            Name:   "api-backend",
            Listen: "0.0.0.0:8081",
            Expose: "http://api.example.com",
        },
    }

    // Set global handler
    configs.SetHandlerFunc(createHandler)

    // Validate all configurations
    if err := configs.Validate(); err != nil {
        panic(err)
    }

    // Create pool
    p, err := configs.Pool(nil, nil, nil)
    if err != nil {
        panic(err)
    }

    // Start all
    p.Start(context.Background())
    defer p.Stop(context.Background())
}

Advanced Filtering:

package main

import (
    "github.com/nabbar/golib/httpserver/pool"
    "github.com/nabbar/golib/httpserver/types"
)

func main() {
    p := pool.New(nil, handler)
    // ... add servers ...

    // Filter by name pattern
    apiServers := p.Filter(types.FieldName, "api-*", "")

    // Filter by bind address
    localServers := p.Filter(types.FieldBind, "127.0.0.1:*", "")

    // Filter by expose URL with regex
    httpsServers := p.Filter(types.FieldExpose, "", `^https://`)

    // List server names
    names := p.List(types.FieldName, types.FieldName, "*", "")
    for _, name := range names {
        println("Server:", name)
    }

    // Walk through servers
    p.Walk(func(bindAddr string, srv httpserver.Server) bool {
        println(srv.GetName(), "at", bindAddr)
        return true  // continue iteration
    })
}

Pool Cloning and Merging:

package main

import (
    "context"
    "github.com/nabbar/golib/httpserver/pool"
)

func main() {
    // Original pool
    p1 := pool.New(nil, handler)
    // ... add servers ...

    // Clone for different context
    ctx2 := context.Background()
    p2 := p1.Clone(ctx2)  // Independent copy

    // Create another pool
    p3 := pool.New(nil, handler)
    // ... add different servers ...

    // Merge p3 into p1
    if err := p1.Merge(p3, nil); err != nil {
        panic(err)
    }

    // p1 now contains servers from both pools
}

types Subpackage

Shared type definitions and constants used across the package.

Handler Types

// FuncHandler is the function signature for handler registration
// Returns a map of handler keys to http.Handler instances
type FuncHandler func() map[string]http.Handler

// BadHandler is a default handler that returns 500 Internal Server Error
type BadHandler struct{}

// NewBadHandler creates a new BadHandler instance
func NewBadHandler() http.Handler

Field Types

// FieldType identifies server fields for filtering operations
type FieldType uint8

const (
    FieldName   FieldType = iota  // Server name field
    FieldBind                      // Bind address field
    FieldExpose                    // Expose URL field
)

Constants

const (
    // HandlerDefault is the default handler key
    HandlerDefault = "default"
    
    // BadHandlerName is the identifier for the bad handler
    BadHandlerName = "no handler"
    
    // TimeoutWaitingPortFreeing is the timeout for port availability checks
    TimeoutWaitingPortFreeing = 250 * time.Microsecond
    
    // TimeoutWaitingStop is the default graceful shutdown timeout
    TimeoutWaitingStop = 5 * time.Second
)

Best Practices

Configuration Management

// ✅ Good: Validate before use
cfg := httpserver.Config{
    Name:   "production-api",
    Listen: "0.0.0.0:8080",
    Expose: "https://api.production.com",
}

if err := cfg.Validate(); err != nil {
    log.Fatalf("Invalid config: %v", err)
}

srv, err := httpserver.New(cfg, logger.Default)
if err != nil {
    log.Fatalf("Failed to create server: %v", err)
}

// ❌ Bad: Skip validation
srv, _ := httpserver.New(cfg, nil)  // May fail at runtime

Graceful Shutdown

// ✅ Good: Context with timeout
func main() {
    srv, _ := httpserver.New(cfg, nil)
    srv.Start(context.Background())

    // Wait for signal
    <-shutdownChan

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Stop(ctx); err != nil {
        log.Printf("Error stopping server: %v", err)
    }
}

// ❌ Bad: Abrupt termination
srv.Stop(context.Background())  // No timeout, may hang
os.Exit(0)  // Abrupt exit without cleanup

Error Handling

// ✅ Good: Check all errors
if err := srv.Start(ctx); err != nil {
    log.Printf("Failed to start: %v", err)
    return err
}

if srv.IsError() {
    log.Printf("Server error: %v", srv.GetError())
}

// ❌ Bad: Ignore errors
srv.Start(ctx)  // Silent failure

Pool Management

// ✅ Good: Centralized error handling
if err := pool.Start(ctx); err != nil {
    // Aggregated errors from all servers
    log.Fatalf("Pool start failed: %v", err)
}

// ✅ Good: Check individual servers
pool.Walk(func(bind string, srv httpserver.Server) bool {
    if !srv.IsRunning() {
        log.Printf("Server %s not running", srv.GetName())
    }
    return true
})

// ❌ Bad: Assume all started
pool.Start(ctx)
// No verification

Handler Registration

// ✅ Good: Register before creation
cfg.RegisterHandlerFunc(handlerFunc)
srv, _ := httpserver.New(cfg, nil)

// ✅ Good: Register after creation
srv.Handler(handlerFunc)

// ❌ Bad: No handler registered
srv, _ := httpserver.New(cfg, nil)
srv.Start(ctx)  // Will use BadHandler (returns 500)

Testing

The package includes comprehensive test coverage using Ginkgo v2 and Gomega.

Test Statistics

Package Tests Coverage Status
httpserver 83/84 53.8% 98.8% Pass (1 skipped)
httpserver/pool 79/79 63.7% All Pass
httpserver/types 32/32 100.0% All Pass
Total 194/195 ~60% 99.5% Pass

Test Categories

  • Configuration Tests: Validation, cloning, edge cases
  • Server Tests: Lifecycle, info methods, TLS detection
  • Handler Tests: Registration, execution, replacement
  • Pool Tests: CRUD operations, filtering, merging
  • Integration Tests: Actual HTTP servers (build tag: integration)

Running Tests

# Run all unit tests
go test -v ./...

# With coverage
go test -v -cover ./...

# Generate coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# Run integration tests (starts actual servers)
go test -tags=integration -v -timeout 120s ./...

# Using Ginkgo CLI
ginkgo -v -r

# With race detection
go test -race -v ./...

# Ginkgo with integration tests
ginkgo -v -r --tags=integration --timeout=2m

See TESTING.md for detailed testing documentation.


Monitoring

Single Server Monitoring

// Basic server information
monitorName := srv.MonitorName()  // Unique monitor identifier
name := srv.GetName()             // Server name
bind := srv.GetBindable()         // Bind address
expose := srv.GetExpose()         // Expose URL
isRunning := srv.IsRunning()      // Running state
uptime := srv.Uptime()            // Time since start

// Monitor interface (requires version info)
monitor, err := srv.Monitor(version)
if err != nil {
    log.Printf("Monitor error: %v", err)
}

Pool Monitoring

// Aggregate pool information
names := pool.MonitorNames()    // All monitor names
isRunning := pool.IsRunning()   // True if any server running
maxUptime := pool.Uptime()      // Maximum uptime across servers
poolSize := pool.Len()          // Number of servers

// Iterate through servers
pool.Walk(func(bindAddr string, srv httpserver.Server) bool {
    log.Printf("Server: %s, Running: %v, Uptime: %v",
        srv.GetName(), srv.IsRunning(), srv.Uptime())
    return true  // continue
})

// Get all monitoring data
monitors, err := pool.Monitor(version)
if err != nil {
    log.Printf("Pool monitor error: %v", err)
}

Error Handling

The package uses typed errors with diagnostic codes:

Error Types

Error Code Description Context
ErrorParamEmpty Required parameter missing Configuration
ErrorHTTP2Configure HTTP/2 setup failed Server initialization
ErrorServerValidate Invalid server configuration Validation
ErrorServerStart Failed to start server Startup
ErrorPortUse Port already in use Port binding
ErrorPoolAdd Failed to add server to pool Pool management
ErrorPoolValidate Pool configuration invalid Validation
ErrorPoolStart Pool start failed Startup
ErrorPoolStop Pool stop failed Shutdown
ErrorPoolRestart Pool restart failed Restart
ErrorPoolMonitor Monitoring failed Monitoring

Error Handling Examples

// Check specific error types
if err := srv.Start(ctx); err != nil {
    if errors.Is(err, ErrorPortUse) {
        log.Println("Port already in use")
    } else if errors.Is(err, ErrorServerValidate) {
        log.Println("Configuration invalid")
    }
    return err
}

// Pool error aggregation
if err := pool.Start(ctx); err != nil {
    // err contains all individual server errors
    log.Printf("Pool start errors: %v", err)
}

// Check server error state
if srv.IsError() {
    log.Printf("Server error: %v", srv.GetError())
}

Troubleshooting

Server Won't Start

// Check disabled flag
if srv.IsDisable() {
    log.Println("Server is disabled in configuration")
}

// Check TLS configuration
if cfg.TLSMandatory && !srv.IsTLS() {
    log.Println("TLS is mandatory but not properly configured")
}

// Check port availability
if err := srv.PortInUse(ctx, cfg.Listen); err == nil {
    log.Println("Port is already in use")
}

// Check if already running
if srv.IsRunning() {
    log.Println("Server is already running")
}

Pool Issues

// Check pool state
log.Printf("Pool size: %d servers", pool.Len())

// Verify server exists
if !pool.Has("127.0.0.1:8080") {
    log.Println("Server not found at this address")
}

// List all servers
names := pool.List(types.FieldName, types.FieldName, "*", "")
for _, name := range names {
    log.Printf("Found server: %s", name)
}

// Check individual server status
pool.Walk(func(bind string, srv httpserver.Server) bool {
    if !srv.IsRunning() {
        log.Printf("Server %s at %s is not running", srv.GetName(), bind)
    }
    return true
})

Configuration Errors

cfg := httpserver.Config{
    Name: "test-server",
    // Missing required fields: Listen, Expose
}

if err := cfg.Validate(); err != nil {
    // err contains detailed validation failures
    log.Printf("Configuration errors: %v", err)
    // Example output: "Listen: required field missing"
}

Contributing

Contributions are welcome! Please follow these guidelines:

Code Contributions

  • Do not use AI to generate package implementation code
  • AI may assist with tests, documentation, and bug fixing
  • All contributions must be thread-safe
  • Pass all tests including race detection: go test -race ./...
  • Maintain or improve test coverage (≥40%)
  • Follow existing code style and patterns

Documentation

  • Update README.md for new features
  • Add code examples for common use cases
  • Keep TESTING.md synchronized with test changes
  • Include GoDoc comments for all public APIs

Testing

  • Write tests for all new features (Ginkgo/Gomega)
  • Test edge cases and error conditions
  • Verify thread safety with race detector
  • Add integration tests with integration build tag when appropriate

Pull Requests

  • Provide clear description of changes
  • Reference related issues
  • Include test results (unit + integration + race)
  • Update documentation

Future Enhancements

Potential improvements for future versions:

Protocol Support

  • HTTP/3 (QUIC) support
  • WebSocket upgrade handling
  • Server-Sent Events (SSE)

Advanced Features

  • Hot reload configuration without restart
  • Dynamic TLS certificate rotation
  • Request/response middleware chain
  • Rate limiting per server
  • Automatic Let's Encrypt integration

Monitoring & Observability

  • Prometheus metrics endpoint
  • Distributed tracing integration (OpenTelemetry)
  • Structured access logs
  • Performance profiling endpoints

High Availability

  • Health check probes (liveness, readiness)
  • Circuit breaker integration
  • Automatic failover in pool
  • Load balancing across pool members

Developer Experience

  • Configuration hot-reload watcher
  • CLI tool for server management
  • Web UI for pool visualization
  • More integration test helpers

Suggestions are welcome via GitHub issues.


AI Transparency Notice

In accordance with Article 50.4 of the EU AI Act, AI assistance has been used for testing, documentation, and bug fixing under human supervision.


License

MIT License - See LICENSE file for details.


Resources