JuliusBrussee/caveman · error

invalid listen port %q

Error message

invalid listen port %q

What it means

Returned by runstate.PortFromListen when the listen string split into host:port but the port portion is not a decimal integer in 1-65535. The run-state filename is derived from the port, so it must be a real TCP port number.

Source

Thrown at proxy/internal/runstate/runstate.go:59

	PID            int       `json:"pid,omitempty"`
	Port           int       `json:"port,omitempty"`
	StartedAt      time.Time `json:"started_at,omitempty"`
	Version        string    `json:"version,omitempty"`
	RecoveryViaMCP bool      `json:"recovery_via_mcp"`
}

func Unknown() PublicState {
	return PublicState{Owner: "unknown"}
}

func PortFromListen(listen string) (int, error) {
	_, raw, err := net.SplitHostPort(listen)
	if err != nil {
		return 0, fmt.Errorf("invalid listen address %q: %w", listen, err)
	}
	port, err := strconv.Atoi(raw)
	if err != nil || port < 1 || port > 65535 {
		return 0, fmt.Errorf("invalid listen port %q", raw)
	}
	return port, nil
}

func Path(home string, port int) string {
	return filepath.Join(home, "run", strconv.Itoa(port)+".json")
}

func New(listen, mode, owner, version string) (State, error) {
	port, err := PortFromListen(listen)
	if err != nil {
		return State{}, err
	}
	if owner != "wrap" && owner != "start" {
		owner = "start"
	}
	var token [16]byte
	if _, err := rand.Read(token[:]); err != nil {

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Use a concrete port number between 1 and 65535, e.g. "127.0.0.1:8397"
  2. If you wanted an ephemeral port, resolve it first (net.Listen, get the bound port) and pass the resolved address
  3. Fix typos and ensure env-derived port variables are set and numeric
  4. Lint the config at startup: strconv.Atoi + range check before runstate.New

Example fix

// before
listen := "127.0.0.1:0" // hope for random port

// after
l, _ := net.Listen("tcp", "127.0.0.1:0")
port := l.Addr().(*net.TCPAddr).Port
listen := fmt.Sprintf("127.0.0.1:%d", port)
Defensive patterns

Strategy: validation

Validate before calling

func normalizePort(listen string) (string, error) {
    _, raw, err := net.SplitHostPort(listen)
    if err != nil { return "", err }
    p, err := strconv.Atoi(raw)
    if err != nil || p < 1 || p > 65535 {
        return "", fmt.Errorf("port %q must be 1-65535", raw)
    }
    return fmt.Sprintf("127.0.0.1:%d", p), nil
}

Type guard

func isValidPort(s string) bool {
    _, raw, err := net.SplitHostPort(s)
    if err != nil { return false }
    p, err := strconv.Atoi(raw)
    return err == nil && p >= 1 && p <= 65535
}

Try / catch

_, err := runstate.New(listen, mode, owner, version)
if err != nil && strings.Contains(err.Error(), "invalid listen port") {
    // reject config early with a clear message instead of half-starting the server
}

Prevention

When it happens

Trigger: Passing a listen string like "localhost:http" (service name instead of number), "localhost:0", "localhost:70000", or "localhost:8o80" (typo letter) into runstate.New or PortFromListen.

Common situations: Copy-paste of a named port from documentation or nginx-style configs; port 0 chosen for 'random port' semantics which runstate rejects; a typo in a config file (letter O for zero); a port computed from an env var that is unset and rendered as garbage.

Related errors


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