derailed/k9s · error

not a metav1.Table

Error message

not a metav1.Table

What it means

Workload.fetch powers the aggregated workloads view: it asks Table.List (server-side Table conversion, Accept: as=Table) for each workload GVR and asserts the first returned object is *metav1.Table. If the API server (or something in between) returns a plain object instead of a Table, the type assertion fails. It also fails when the table row is a custom type injected by a custom view decorator.

Source

Thrown at internal/dao/workload.go:89

	if client.IsClusterScoped(ns) {
		return dial.Delete(ctx, n, opts)
	}

	return dial.Namespace(ns).Delete(ctx, n, opts)
}

func (a *Workload) fetch(ctx context.Context, gvr *client.GVR, ns string) (*metav1.Table, error) {
	a.gvr = gvr
	oo, err := a.Table.List(ctx, ns)
	if err != nil {
		return nil, err
	}
	if len(oo) == 0 {
		return nil, fmt.Errorf("no table found for gvr: %s", gvr)
	}
	tt, ok := oo[0].(*metav1.Table)
	if !ok {
		return nil, errors.New("not a metav1.Table")
	}

	return tt, nil
}

// List fetch workloads.
func (a *Workload) List(ctx context.Context, ns string) ([]runtime.Object, error) {
	oo := make([]runtime.Object, 0, 100)
	for _, gvr := range resList {
		table, err := a.fetch(ctx, gvr, ns)
		if err != nil {
			return nil, err
		}
		var (
			ns string
			ts metav1.Time
		)
		for _, r := range table.Rows {

View on GitHub (pinned to 2d3ccc6ba2)

Solutions

  1. Verify the server supports server-side tables: kubectl get <resource> -o json with Accept as=Table, or `kubectl get --raw` with the Table accept header.
  2. Bypass middlewares/proxies that rewrite API responses and retry the workloads view.
  3. If implementing a custom Table DAO, make sure List returns []*metav1.Table as the stock implementation does.

Example fix

// before
tt, ok := oo[0].(*metav1.Table)
if !ok { return nil, errors.New("not a metav1.Table") }
// after
tt, ok := oo[0].(*metav1.Table)
if !ok { return nil, fmt.Errorf("gvr %s: expected *metav1.Table, got %T", gvr, oo[0]) }
Defensive patterns

Strategy: type-guard

Validate before calling

if len(oo) == 0 { return nil, fmt.Errorf("no table found for gvr: %s", gvr) } // already guarded upstream
// verify server table support once per session
tt, ok := oo[0].(*metav1.Table)
if !ok { return nil, fmt.Errorf("gvr %s: got %T, want *metav1.Table", gvr, oo[0]) }

Type guard

func isMetaTable(o runtime.Object) bool {
    _, ok := o.(*metav1.Table)
    return ok
}

Try / catch

tbl, err := wl.fetch(ctx, gvr, ns)
if err != nil {
    if strings.Contains(err.Error(), "not a metav1.Table") { /* server/proxy not honoring Table accept header */ }
    return err
}

Prevention

When it happens

Trigger: An API server or aggregated API that ignores the Table accept header and returns the resource list unconverted; a proxy/stripping proxy rewriting responses; cached/stale results from a custom dao.Table implementation returning non-metav1.Table runtime.Objects.

Common situations: Very old or non-standard Kubernetes distributions; API server behind mutating middlewares; bugs in custom resource views that replace the Table DAO's output; k9s version expecting Table semantics the server does not support.

Related errors


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