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
- Use one of the exact NodeMetricKeys values (inspect service.NodeMetricKeys in your build, e.g. cpu, mem, netUp, netDown).
- After a panel upgrade, refresh the frontend so the metric tabs match the backend allowlist.
- 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
- Keep metric name lists in one shared module used by both API client and UI tabs.
- Treat 400 invalid metric as a contract mismatch: re-sync with the deployed backend version.
- Never construct metric URLs from free-text input.
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.