JuliusBrussee/caveman · error

listen address %q must be loopback host:port

Error message

listen address %q must be loopback host:port

What it means

config.validateListen rejects the proxy's listen address before startup when it cannot be split into a host and a non-empty port. The caveman standalone proxy is unauthenticated BYOK, so the address must be a concrete loopback host:port. Malformed input such as '127.0.0.1' (no port), ':8080' (no host usable later), or extra colons fails here.

Source

Thrown at proxy/internal/config/config.go:126

		}
	}
	cfg = cfg.withDefaults()
	if err := validateListen(cfg.Listen); err != nil {
		return Config{}, err
	}
	if err := cfg.validateCompat(); err != nil {
		return Config{}, err
	}
	return cfg, nil
}

// validateListen keeps standalone's unauthenticated BYOK proxy local to one
// operator. Binding an empty, wildcard, or non-loopback host would expose every
// configured provider credential to the network with no inbound authentication.
func validateListen(listen string) error {
	host, port, err := net.SplitHostPort(strings.TrimSpace(listen))
	if err != nil || port == "" {
		return fmt.Errorf("listen address %q must be loopback host:port", listen)
	}
	if strings.EqualFold(host, "localhost") {
		return nil
	}
	ip := net.ParseIP(host)
	if ip == nil || !ip.IsLoopback() {
		return fmt.Errorf("listen address %q is not loopback; standalone proxy has no inbound authentication", listen)
	}
	return nil
}

func (c Config) withDefaults() Config {
	if label := env.String("CAVEMAN_LABEL", ""); label != "" {
		c.Label = label
	}
	if c.Label == "" {
		c.Label = "local"
	}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Set listen to an explicit loopback host:port, e.g. '127.0.0.1:8080', 'localhost:8080', or '[::1]:8080'
  2. If you need a different port only, keep the host loopback and change just the port
  3. Wrap IPv6 loopback in brackets so SplitHostPort parses the port correctly
  4. To expose the proxy beyond loopback, put an authenticated reverse proxy in front instead of widening the bind address

Example fix

// before
listen: "127.0.0.1"   // Error[1060]: must be loopback host:port

// after
listen: "127.0.0.1:8080"
Defensive patterns

Strategy: validation

Validate before calling

// validateListenLike mirrors config.validateListen's parse step.
func checkListenAddr(listen string) error {
    host, port, err := net.SplitHostPort(strings.TrimSpace(listen))
    if err != nil || port == "" {
        return fmt.Errorf("listen %q: missing host:port", listen)
    }
    return nil
}

if err := checkListenAddr(cfg.Listen); err != nil { /* fix config before start */ }

Prevention

When it happens

Trigger: Setting listen (caveman.yaml or CAVEMAN_LISTEN env) to a host without a port ('localhost'), a port without a host (':9111' parses but usually fails the loopback check), '127.0.0.1:8080:extra', or a port string that SplitHostPort rejects entirely.

Common situations: Copying a bare hostname from docs into the listen field; Docker-minded habits of binding ':PORT'; trailing whitespace variants that SplitHostPort still fails on; IPv6 addresses written without brackets ('::1:8080' instead of '[::1]:8080').

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/2255303a268092dd. Report an issue: GitHub.