MHSanaei/3x-ui · error

invalid metric

Error message

invalid metric

What it means

NodeController.history validates c.Param("metric") against the allowlist service.NodeMetricKeys; anything not in the list returns 'invalid metric' with detail 'unknown metric'. The endpoint then aggregates a node metric over time buckets. The allowlist exists both for correctness and to prevent arbitrary keys from reaching the aggregation query.

Source

Thrown at internal/web/controller/node.go:349

		return
	}
	if len(req.Ids) == 0 {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), fmt.Errorf("no nodes selected"))
		return
	}
	results, err := a.nodeService.UpdatePanels(req.Ids, req.Dev)
	jsonMsgObj(c, I18nWeb(c, "pages.nodes.toasts.updateStarted"), results, err)
}

func (a *NodeController) history(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		jsonMsg(c, I18nWeb(c, "get"), err)
		return
	}
	metric := c.Param("metric")
	if !slices.Contains(service.NodeMetricKeys, metric) {
		jsonMsg(c, "invalid metric", fmt.Errorf("unknown metric"))
		return
	}
	bucket, err := strconv.Atoi(c.Param("bucket"))
	if err != nil || bucket <= 0 || !service.IsAllowedHistoryBucket(bucket) {
		jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket"))
		return
	}
	jsonObj(c, a.nodeService.AggregateNodeMetric(id, metric, bucket, 60), nil)
}

View on GitHub (pinned to ad32144c42)

Solutions

  1. Use one of the exact NodeMetricKeys values (inspect service.NodeMetricKeys in your build, e.g. cpu, mem, netUp, netDown).
  2. After a panel upgrade, refresh the frontend so the metric tabs match the backend allowlist.
  3. For API consumers, fetch the key list once and drive the UI/script from it rather than hardcoding.

Example fix

# before
GET /panel/api/nodes/history/1/cpuPercent/60   # invalid metric

# after
GET /panel/api/nodes/history/1/cpu/60
Defensive patterns

Strategy: type-guard

Validate before calling

// derive allowed list from the API contract, not guesses
const NODE_METRIC_KEYS = ['cpu', 'mem', 'netUp', 'netDown'] as const
type NodeMetric = (typeof NODE_METRIC_KEYS)[number]
function isNodeMetric(m: string): m is NodeMetric {
  return (NODE_METRIC_KEYS as readonly string[]).includes(m)
}

Type guard

function isNodeMetric(m: string): m is NodeMetric {
  return (NODE_METRIC_KEYS as readonly string[]).includes(m)
}

Prevention

When it happens

Trigger: GET /panel/api/nodes/history/:id/:metric/:bucket with a metric segment not in NodeMetricKeys — e.g. 'cpuPercent' when the key is 'cpu', or a newly added metric the running binary predates.

Common situations: Frontend and backend versions drift (new metric tabs shipped before the backend); typo in a manual API call; a script guessing metric names; metric renamed in a release.

Related errors


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/d54fdad4a79a825e. Report an issue: GitHub.