Skip to main content

Overview

The backend routing system supports flexible request routing based on hostnames and URL paths. It provides both legacy and new configuration formats with round-robin load balancing.

Types

Backend

Represents a backend server pool with load balancing.
main.go:24-27
type Backend struct {
    Addrs   []string
    Counter uint64
}
Addrs
[]string
List of backend addresses (e.g., ["web:80", "web2:80"])
Counter
uint64
Atomic counter for round-robin load balancing

HostBackend

Defines routing configuration for a specific host.
main.go:42-45
type HostBackend struct {
    Default *Backend
    Paths   map[string]*Backend // key = prefix, e.g. "/static"
}
Default
*Backend
Default backend for requests that don’t match any path prefix
Paths
map[string]*Backend
Map of URL path prefixes to specific backends. Longest prefix wins.

Functions

loadBackendsFromEnv()

Loads backend configurations from the BACKENDS environment variable or assigns a default backend.
main.go:98-161
func loadBackendsFromEnv() (map[string]*HostBackend, error)
return
map[string]*HostBackend
Map of hostnames to their backend configurations
error
error
Error if JSON parsing fails

Supported Formats

New Format (Recommended) - Supports path-based routing:
{
  "example.com": {
    "default": ["web:80", "web2:80"],
    "paths": {
      "/static": ["static:80"],
      "/api": ["api:8080"]
    }
  }
}
Legacy Format - Host-only routing:
{
  "example.com": ["web:80"],
  "api.example.com": ["api:8080"],
  "default": ["localhost:5000"]
}

Default Behavior

If BACKENDS is empty or not set:
main.go:100-106
if strings.TrimSpace(raw) == "" {
    return map[string]*HostBackend{
        "default": {
            Default: &Backend{Addrs: []string{"localhost:5000"}},
            Paths:   map[string]*Backend{},
        },
    }, nil
}
The loader automatically falls back to using the first path backend if no default is specified in the new format (lines 133-140).

pickBackendForRequest()

Selects the appropriate backend for a request based on path prefix matching.
main.go:317-341
func pickBackendForRequest(hostCfg *HostBackend, path string) *Backend {
    if hostCfg == nil {
        return nil
    }

    // Match longest prefix (important if you have /static and /static/img)
    var (
        bestLen int
        best    *Backend
    )
    for pfx, be := range hostCfg.Paths {
        if strings.HasPrefix(path, pfx) && len(pfx) > bestLen {
            bestLen = len(pfx)
            best = be
        }
    }
    if best != nil && len(best.Addrs) > 0 {
        return best
    }

    if hostCfg.Default != nil && len(hostCfg.Default.Addrs) > 0 {
        return hostCfg.Default
    }
    return nil
}
hostCfg
*HostBackend
required
Host-specific backend configuration
path
string
required
URL path from the request (e.g., /static/image.png)
return
*Backend
Selected backend or nil if none found

Matching Logic

  1. Longest Prefix Match: Finds the longest matching path prefix from Paths map
  2. Default Fallback: Uses Default backend if no path matches
  3. Nil Return: Returns nil if no valid backend is configured

Usage Example

From main.go:541-553:
// Get host configuration
hostCfg, ok := backends[hostOnly]
if !ok {
    hostCfg = backends["default"]
}

// Pick backend based on path
be := pickBackendForRequest(hostCfg, r.URL.Path)
if be == nil || len(be.Addrs) == 0 {
    http.Error(w, "Bad Gateway: backend not configured", http.StatusBadGateway)
    return
}

// Round-robin selection
idx := int((atomic.AddUint64(&be.Counter, 1) - 1) % uint64(len(be.Addrs)))
target := be.Addrs[idx]

splitHostPort()

Extracts the host and port from a network address string. Defaults to port 80 if not specified.
main.go:190-199
func splitHostPort(addr string) (string, int) {
    host, portStr, err := net.SplitHostPort(addr)
    if err == nil {
        port, _ := strconv.Atoi(portStr)
        return host, port
    }

    // No port → use default
    return addr, 80
}
addr
string
required
Network address (e.g., "192.168.1.1:8080" or "example.com")
host
string
Hostname or IP address
port
int
Port number (defaults to 80 if not specified)

Usage Example

// From main.go:488-490
clientIP, clientPort := splitHostPort(r.RemoteAddr)
serverIP, serverPort := splitHostPort(r.Host)
tx.ProcessConnection(clientIP, clientPort, serverIP, serverPort)

Load Balancing

Round-Robin Algorithm

Backends with multiple addresses use atomic counter-based round-robin:
main.go:552-553
idx := int((atomic.AddUint64(&be.Counter, 1) - 1) % uint64(len(be.Addrs)))
target := be.Addrs[idx]
This ensures thread-safe distribution across backend servers.

Environment Variables

See Environment Variables for configuration:
  • BACKENDS - JSON configuration for backend routing

Complete Example

export BACKENDS='{
  "example.com": {
    "default": ["web1:80", "web2:80"],
    "paths": {
      "/static": ["cdn:80"],
      "/api/v1": ["api1:8080", "api2:8080"]
    }
  },
  "admin.example.com": {
    "default": ["admin:3000"]
  },
  "default": {
    "default": ["localhost:5000"]
  }
}'
Routing behavior:
  • example.com/static/logo.pngcdn:80
  • example.com/api/v1/usersapi1:8080 or api2:8080 (round-robin)
  • example.com/aboutweb1:80 or web2:80 (round-robin)
  • admin.example.com/dashboardadmin:3000
  • unknown.com/pagelocalhost:5000 (default)

Build docs developers (and LLMs) love