derailed/k9s · error

expecting a ForwardRes but got %T

Error message

expecting a ForwardRes but got %T

What it means

PortForward.Render implements the k9s renderer contract for the port-forward view: it requires the object handed to it to be a render.ForwardRes value — the synthetic row type (an embedded Forwarder plus BenchCfg) the port-forward browser builds per tunnel. The comma-ok assertion on the any-typed parameter fails and returns this error, printing the concrete Go type (%T) that actually arrived. A mismatch means the producer feeding this renderer emitted something other than ForwardRes, so the row cannot be drawn. Note the assertion matches the value type, so passing a *ForwardRes pointer also fails.

Source

Thrown at internal/render/portforward.go:71

func (PortForward) Header(string) model1.Header {
	return model1.Header{
		model1.HeaderColumn{Name: "NAMESPACE"},
		model1.HeaderColumn{Name: "NAME"},
		model1.HeaderColumn{Name: "CONTAINER"},
		model1.HeaderColumn{Name: "PORTS"},
		model1.HeaderColumn{Name: "URL"},
		model1.HeaderColumn{Name: "C"},
		model1.HeaderColumn{Name: "N"},
		model1.HeaderColumn{Name: "VALID", Attrs: model1.Attrs{Wide: true}},
		model1.HeaderColumn{Name: "AGE", Attrs: model1.Attrs{Time: true}},
	}
}

// Render renders a K8s resource to screen.
func (PortForward) Render(o any, _ string, r *model1.Row) error {
	pf, ok := o.(ForwardRes)
	if !ok {
		return fmt.Errorf("expecting a ForwardRes but got %T", o)
	}

	ports := strings.Split(pf.Port(), ":")
	r.ID = pf.ID()
	ns, n := client.Namespaced(r.ID)

	r.Fields = model1.Fields{
		ns,
		trimContainer(n),
		pf.Container(),
		pf.Port(),
		UrlFor(pf.Config.Host, pf.Config.Path, ports[0], pf.Address()),
		AsThousands(int64(pf.Config.C)),
		AsThousands(int64(pf.Config.N)),
		"",
		ToAge(metav1.Time{Time: pf.Age()}),
	}

View on GitHub (pinned to 2d3ccc6ba2)

Solutions

  1. Ensure the port-forward view passes render.ForwardRes by value (not a pointer) to Render
  2. Fix the view factory mapping so PortForward renders only the port-forward view's rows
  3. Log the %T from the error to find the producer emitting the wrong type and fix it there
  4. In your own code, use the comma-ok assertion before calling Render and skip the row instead of erroring

Example fix

// before
err := pfRenderer.Render(&render.ForwardRes{Forwarder: f, Config: cfg}, ns, row)
// after
err := pfRenderer.Render(render.ForwardRes{Forwarder: f, Config: cfg}, ns, row)
Defensive patterns

Strategy: type-guard

Validate before calling

// gate before invoking the port-forward renderer
if _, ok := o.(render.ForwardRes); !ok {
	return fmt.Errorf("port-forward view requires render.ForwardRes value, got %T", o)
}
err := pfRenderer.Render(o, ns, row)

Type guard

func isForwardRes(o any) bool {
	_, ok := o.(render.ForwardRes)
	return ok
}

Try / catch

if err := pfRenderer.Render(o, ns, row); err != nil {
	slog.Warn("port-forward row skipped", "type", fmt.Sprintf("%T", o), slogs.Error, err)
	return nil // drop the row, keep the view alive; never panic the UI over one bad object
}

Prevention

When it happens

Trigger: Invoking PortForward{}.Render(o, ns, row) with o being a *ForwardRes pointer, a raw *v1.Pod, an *unstructured.Unstructured, or any custom row type; wiring the portforward renderer to a different GVR in a custom view factory; plugins that list port-forwards from their own data source and forward those objects straight into Render.

Common situations: Contributing a new port-forward browser or plugin; refactoring Forwarder aggregation so the view now wraps tunnels in a different struct; unit tests passing pod fixtures instead of ForwardRes; copy-pasting a renderer registration into a custom view without matching the payload type.

Related errors


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