Reverse Proxy

Built-in reverse proxy with 5 load balancing strategies, health checks, connection pooling, response caching, and request interception hooks.

Basic Setup

Add a proxy domain to forward traffic from a hostname to one or more backends:

go
s := core.New(core.Config{
    Addr: ":443",
})

s.AddProxyDomain(core.DomainConfig{
    Domain: "api.example.com",
    Backends: []core.BackendConfig{
        {Addr: "http://localhost:3001"},
        {Addr: "http://localhost:3002"},
    },
})

s.Router.Build()
s.ListenAndServeTLS()

Domain Configuration

Full control over each proxied domain with timeouts, retries, and connection limits:

go
s.AddProxyDomain(core.DomainConfig{
    Domain: "api.example.com",
    Backends: []core.BackendConfig{
        {Addr: "https://backend1.local", Weight: 3},
        {Addr: "https://backend2.local", Weight: 1},
    },
    LoadBalancer:   core.LBWeightedRR,
    ConnectTimeout: 10 * time.Second,
    ReadTimeout:    30 * time.Second,
    WriteTimeout:   10 * time.Second,
    MaxRetries:     2,
    MaxIdleConns:   32,
    IdleTimeout:    90 * time.Second,
    PreserveHost:   true,
    HostHeader:     "backend.internal",
    HealthCheck: core.HealthCheckConfig{
        Enabled:          true,
        Path:             "/health",
        Interval:         10 * time.Second,
        Timeout:          5 * time.Second,
        FailThreshold:    3,
        SuccessThreshold: 2,
    },
})
Option Default Description
Domain Hostname to match incoming requests
Backends List of backend servers with optional weights
LoadBalancer Round Robin Load balancing strategy
ConnectTimeout 10s Timeout to establish backend connection
ReadTimeout 30s Timeout for reading backend response
WriteTimeout 10s Timeout for writing request to backend
MaxRetries 2 Retries on backend failure
MaxIdleConns 32 Connection pool size per backend
IdleTimeout 90s Max idle time for pooled connections
PreserveHost false Forward original Host header
HostHeader "" Override Host header to backend

Load Balancing Strategies

Choose from 5 built-in load balancing algorithms:

LBRoundRobin

Simple round-robin rotation across all healthy backends. Default strategy.

LBWeightedRR

Weighted round-robin. Higher weight = more traffic. Weights 1–100.

LBLeastConn

Sends to the backend with fewest active connections.

LBIPHash

Hash client IP for sticky sessions. Same client always hits the same backend.

LBRandom

Random selection from healthy backends.

go
s.AddProxyDomain(core.DomainConfig{
    Domain: "app.example.com",
    Backends: []core.BackendConfig{
        {Addr: "http://10.0.0.1:8080", Weight: 5},
        {Addr: "http://10.0.0.2:8080", Weight: 3},
        {Addr: "http://10.0.0.3:8080", Weight: 1},
    },
    LoadBalancer: core.LBWeightedRR,
})

Health Checks

Automatic backend health monitoring with configurable thresholds:

go
HealthCheck: core.HealthCheckConfig{
    Enabled:          true,
    Path:             "/health",
    Interval:         10 * time.Second,
    Timeout:          5 * time.Second,
    FailThreshold:    3,
    SuccessThreshold: 2,
}

Two check modes:

  • TCP check (Path = ""): Opens a TCP connection to verify the backend is reachable.
  • HTTP check (Path = "/health"): Sends GET request, expects 2xx-4xx status.

Response Caching

Cache backend responses in memory with automatic gzip pre-compression:

go
s.SetProxyCache(core.ProxyCacheConfig{
    DefaultMaxAge:  5 * time.Minute,
    MaxTotalBytes:  256 * 1024 * 1024,
    MaxEntries:     10000,
    MaxEntrySize:   4 * 1024 * 1024,
    PreCompress:    true,
    CompressLevel:  6,
    CompressMinLen: 256,
    StaleWhileRev:  30 * time.Second,
})
Option Default Description
DefaultMaxAge 5min Default TTL for cached entries
MaxTotalBytes 256MB Total cache memory budget
MaxEntries 10,000 Maximum cached entries
MaxEntrySize 4MB Skip entries larger than this
PreCompress false Store pre-compressed gzip copies
StaleWhileRev 0 Serve stale content while refreshing

Cache Rules

Define per-path caching rules for fine-grained control:

go
s.SetProxyCache(core.ProxyCacheConfig{
    Rules: []core.CacheRule{
        {
            PathPrefix: "/api/v1/",
            MaxAge:     10 * time.Minute,
            Methods:    []string{"GET"},
            StatusOnly: []int{200},
        },
        {
            PathPrefix: "/static/",
            MaxAge:     24 * time.Hour,
            MaxBytes:   10 * 1024 * 1024,
        },
    },
})

Manual Caching in Hooks

go
s.OnProxyResponse(func(pr *core.ProxyResponse) {
    if pr.StatusCode == 200 {
        pr.CacheThis(5*time.Minute, 100,
            core.WithPreCompress(true),
            core.WithCompressMinLen(1024),
        )
    }
})

Interception Hooks

Intercept proxy requests and responses for logging, modification, or blocking:

OnProxyRequest

Modify or block requests before forwarding. Return false to reject the request with 502:

go
s.OnProxyRequest(func(pr *core.ProxyRequest) bool {
    pr.Headers = append(pr.Headers, [2]string{"X-Internal-Auth", "secret"})
    return true
})

OnProxyResponse

Modify response headers, trigger caching, or prevent caching:

go
s.OnProxyResponse(func(pr *core.ProxyResponse) {
    pr.Headers = append(pr.Headers, [2]string{"X-Proxy", "ALOS"})

    if pr.StatusCode == 200 {
        pr.CacheThis(5*time.Minute, 1000)
    }
})

DontCache

Prevent a specific response from being cached, even if it matches cache rules:

go
s.OnProxyResponse(func(pr *core.ProxyResponse) {
    if pr.Path == "/api/user/me" {
        pr.DontCache()
        return
    }

    pr.CacheThis(10*time.Minute, 500)
})

OnProxyError

go
s.OnProxyError(func(pe core.ProxyError) {
    log.Printf("proxy: %s → %s attempt %d: %v",
        pe.Domain, pe.Backend, pe.Attempt, pe.Err)
})

ProxyRequest Fields

Field Type Description
Domain string Matched domain
Backend string Selected backend address
ClientAddr string Client IP
Method string HTTP method
Path string Request path
Headers [][2]string Mutable request headers
Host string Mutable host header

ProxyResponse Fields

Field Type Description
Domain string Matched domain
Backend string Backend that responded
StatusCode int Mutable status code
Headers [][2]string Mutable response headers
Method string Original request method
Path string Original request path

Connection Pooling

ALOS maintains a connection pool per backend to avoid TCP handshake overhead:

  • Channel-based pool: Lock-free get/put operations
  • Idle timeout eviction: Stale connections removed automatically
  • TLS connection age validation: Max 2x idle timeout for TLS conns
  • Configurable pool size: Default 32 idle connections per backend
go
DomainConfig{
    MaxIdleConns: 64,
    IdleTimeout:  120 * time.Second,
}

Cache Purge

Programmatically clear cached responses:

Method Description
PurgeProxyCache(method, host, path string) bool Remove a specific cached response. Returns true if found.
PurgeAllProxyCache() Clear the entire proxy cache
PurgeDomainCache(domain string) int64 Clear all cached entries for a domain. Returns count purged.
ProxyCacheStats() (entries, totalBytes int64, hits, misses uint64) Get cache statistics
go
s.PurgeProxyCache("GET", "api.example.com", "/api/v1/users")

s.PurgeDomainCache("api.example.com")

s.PurgeAllProxyCache()

entries, totalBytes, hits, misses := s.ProxyCacheStats()
fmt.Printf("Cache: %d entries, %d bytes, %d hits, %d misses\n",
    entries, totalBytes, hits, misses)

Expose a cache management endpoint:

go
s.Router.DELETE("/admin/cache/:domain", func(req *core.Request, resp *core.Response) {
    domain := req.ParamValue("domain")
    count := s.PurgeDomainCache(domain)
    resp.JSONMarshal(map[string]any{
        "domain": domain,
        "purged": count,
    })
})

HTTP Backend Routing

Route traffic to different backends based on path prefix (useful for microservices):

go
s.SetHTTPRoutes([]core.HTTPRoute{
    {
        PathPrefix: "/api/",
        Backend:    "http://api-service:3000",
    },
    {
        PathPrefix: "/auth/",
        Backend:    "http://auth-service:4000",
        HostHeader: "auth.internal",
    },
    {
        PathPrefix: "/",
        Backend:    "http://frontend:8080",
    },
})
ESC