Skip to content

Add Configuration Validation #19

@aaydin-tr

Description

@aaydin-tr

Add Configuration Validation

🔧 Priority: HIGH

Labels: enhancement, reliability, config, validation
Estimated Effort: 1 day
Assignee: @aaydin-tr

Problem Description

Currently, Divisor has minimal configuration validation, which can lead to runtime crashes or unexpected behavior when users provide invalid configurations. There's no comprehensive validation to catch common configuration mistakes before the service starts.

Current Issues:

  1. Missing port validation - Non-numeric ports cause runtime errors
  2. No URL validation - Malformed backend URLs aren't caught
  3. Incomplete required field checks - Missing required fields cause panics
  4. No range validation - Negative or zero values for timeouts/connections
  5. Poor error messages - Errors don't guide users to fix issues

Proposed Solution

Add comprehensive configuration validation with clear, helpful error messages that guide users to fix configuration issues.

Implementation Plan

1. Enhance Configuration Validation

File: pkg/config/config.go

Add detailed validation to the PrepareConfig() method:

import (
    "errors"
    "fmt"
    "net"
    "net/url"
    "strconv"
    "strings"
    "time"
)

func (c *Config) PrepareConfig() error {
    // Validate core server configuration
    if err := c.validateServerConfig(); err != nil {
        return fmt.Errorf("server configuration error: %w", err)
    }

    // Validate load balancer configuration
    if err := c.validateLoadBalancerConfig(); err != nil {
        return fmt.Errorf("load balancer configuration error: %w", err)
    }

    // Validate backend configuration
    if err := c.validateBackends(); err != nil {
        return fmt.Errorf("backend configuration error: %w", err)
    }

    // Validate monitoring configuration
    if err := c.validateMonitoring(); err != nil {
        return fmt.Errorf("monitoring configuration error: %w", err)
    }

    // Validate custom headers
    if err := c.validateCustomHeaders(); err != nil {
        return fmt.Errorf("custom headers configuration error: %w", err)
    }

    // Set defaults after validation
    c.setDefaults()

    return nil
}

func (c *Config) validateServerConfig() error {
    // Validate host
    if c.Host == "" {
        c.Host = "localhost" // Set default
    }

    // Validate port
    if c.Port == "" {
        return errors.New("port is required")
    }

    port, err := strconv.Atoi(c.Port)
    if err != nil {
        return fmt.Errorf("port must be a valid number, got '%s'", c.Port)
    }

    if port < 1 || port > 65535 {
        return fmt.Errorf("port must be between 1 and 65535, got %d", port)
    }

    // Check if port is available
    if c.Host != "" {
        addr := net.JoinHostPort(c.Host, c.Port)
        listener, err := net.Listen("tcp", addr)
        if err != nil {
            return fmt.Errorf("cannot bind to %s: %w", addr, err)
        }
        listener.Close()
    }

    return nil
}

2. Add Backend URL Validation

func (c *Config) validateBackend(index int, backend *Backend) error {
    // Validate URL
    if backend.Url == "" {
        return errors.New("url is required")
    }

    // Check if URL contains protocol and strip it
    originalURL := backend.Url
    backend.Url = protocolRegex.ReplaceAllString(backend.Url, "")

    // Validate URL format (must contain port)
    if !strings.Contains(backend.Url, ":") {
        return fmt.Errorf("url '%s' must include port (e.g., example.com:80 or example.com:443)", originalURL)
    }

    // Parse and validate host:port
    host, portStr, err := net.SplitHostPort(backend.Url)
    if err != nil {
        return fmt.Errorf("invalid url format '%s': %w", backend.Url, err)
    }

    if host == "" {
        return fmt.Errorf("hostname cannot be empty in url '%s'", backend.Url)
    }

    port, err := strconv.Atoi(portStr)
    if err != nil {
        return fmt.Errorf("invalid port '%s' in url '%s'", portStr, backend.Url)
    }

    if port < 1 || port > 65535 {
        return fmt.Errorf("port %d out of valid range (1-65535) in url '%s'", port, backend.Url)
    }

    return nil
}

Testing Plan

Unit Tests

Create pkg/config/config_test.go:

func TestConfigValidation(t *testing.T) {
    tests := []struct {
        name        string
        config      Config
        expectError bool
        errorSubstr string
    }{
        {
            name: "valid basic config",
            config: Config{
                Type: "round-robin",
                Host: "localhost",
                Port: "8080",
                Backends: []Backend{
                    {Url: "backend1.local:8081"},
                },
            },
            expectError: false,
        },
        {
            name: "missing port",
            config: Config{
                Type: "round-robin",
                Host: "localhost",
                Backends: []Backend{
                    {Url: "backend1.local:8081"},
                },
            },
            expectError: true,
            errorSubstr: "port is required",
        },
        // ... more test cases
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.config.PrepareConfig()
            
            if tt.expectError {
                require.Error(t, err)
                if tt.errorSubstr != "" {
                    assert.Contains(t, err.Error(), tt.errorSubstr)
                }
            } else {
                require.NoError(t, err)
            }
        })
    }
}

Configuration Examples

After implementation, provide clear error messages:

# Invalid configuration
port: "invalid"
backends: []

# Error output:
# server configuration error: port must be a valid number, got 'invalid'
# backend configuration error: at least one backend must be configured

Acceptance Criteria

  • Port validation with clear error messages
  • Backend URL validation (host:port format)
  • Required field validation for all configuration sections
  • Range validation for timeouts and connection limits
  • Load balancer type validation
  • Monitoring configuration validation
  • Helpful error messages that guide users to fix issues
  • Comprehensive unit tests for all validation scenarios
  • Backward compatibility maintained

Files to Modify

  1. pkg/config/config.go - Add validation methods
  2. pkg/config/config_test.go - Add comprehensive validation tests
  3. Examples/ - Add examples showing error handling

Dependencies: None
Follows: Issue #1 (HTTPS Backend Support)
Documentation: Update README with configuration validation examples

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions