derailed/k9s · error

spec to local port count mismatch. Expected %d but got %d

Error message

spec to local port count mismatch. Expected %d but got %d

What it means

Package-level port.ToTunnels(address, specs, localPorts) splits the comma-joined spec string and the comma-joined local-ports string and requires equal counts. The error is an internal consistency check: every port-forward spec must have exactly one resolved local port.

Source

Thrown at internal/port/pfs.go:71

func ParsePFs(ann string) (PFAnns, error) {
	ss := strings.Split(ann, ",")
	pp := make(PFAnns, 0, len(ss))
	for _, s := range ss {
		f, err := ParsePF(s)
		if err != nil {
			return nil, err
		}
		pp = append(pp, f)
	}

	return pp, nil
}

func ToTunnels(address, specs, localPorts string) (PortTunnels, error) {
	pp, lps := strings.Split(specs, ","), strings.Split(localPorts, ",")

	if len(pp) != len(lps) {
		return nil, fmt.Errorf("spec to local port count mismatch. Expected %d but got %d", len(pp), len(lps))
	}

	pts := make(PortTunnels, 0, len(pp))
	for i, p := range pp {
		a, err := ParsePF(p)
		if err != nil {
			return nil, err
		}
		n, err := a.PortNum()
		if err != nil {
			return nil, err
		}
		pts = append(pts, PortTunnel{
			Address:       address,
			Container:     a.Container,
			ContainerPort: n,
			LocalPort:     lps[i],
		})

View on GitHub (pinned to 2d3ccc6ba2)

Solutions

  1. Make the lists 1:1 — add the missing local port or remove the orphaned spec entry
  2. Count entries: echo "$specs" | tr ',' '\n' | wc -l vs the same for localPorts
  3. Re-enable auto port assignment so k9s regenerates a matched pair instead of hand-maintaining it
  4. Restart k9s after fixing so tunnels are rebuilt from the corrected config

Example fix

# before
specs="a::8080:80,b::9090:81"  localPorts="8080"
# after
specs="a::8080:80,b::9090:81"  localPorts="8080,9090"
Defensive patterns

Strategy: validation

Validate before calling

specs := strings.Split(specStr, ",")
ports := strings.Split(localPortsStr, ",")
if len(specs) != len(ports) {
	return fmt.Errorf("config drift: %d specs vs %d local ports", len(specs), len(ports))
}
// safe to call port.ToTunnels(address, specStr, localPortsStr)

Try / catch

Fail fast on the count mismatch — it indicates corrupted config; do not attempt partial tunnel creation from the longer list.

Prevention

When it happens

Trigger: specs='a::8080:80,b::9090:81' with localPorts='8080' (2 specs vs 1 local port). Occurs when the serialized spec list and the auto-assigned/manual local port list get out of sync, e.g. one annotation failed to resolve a port number before serialization.

Common situations: Hand-editing k9s config portForward sections and adding a spec without a matching port; partial serialization when a named port cannot be matched to a container port; merging configs across machines.

Related errors


AI-assisted analysis of derailed/k9s@2d3ccc6ba2 (2026-08-15). Data as JSON: /api/errors/a516f224ab01d002. Report an issue: GitHub.