MHSanaei/3x-ui · error

invalid metric

Error message

invalid metric

What it means

ServerController.getMetricHistoryBucket serves per-tab system metric history and validates c.Param("metric") against service.SystemMetricKeys (cpu, mem, netUp, netDown, online, load1/5/15 per the handler comment). An unknown key returns 'invalid metric' / 'unknown metric' before the bucket is even parsed.

Source

Thrown at internal/web/controller/server.go:140

// getCpuHistoryBucket retrieves aggregated CPU usage history based on the specified time bucket.
// Kept for back-compat; new callers should use /history/cpu/:bucket which
// returns {"t","v"} (uniform across all metrics) instead of {"t","cpu"}.
func (a *ServerController) getCpuHistoryBucket(c *gin.Context) {
	bucket, ok := parseHistoryBucket(c)
	if !ok {
		return
	}
	jsonObj(c, a.serverService.AggregateCpuHistory(bucket, 60), nil)
}

// getMetricHistoryBucket returns up to 60 buckets of history for a single
// system metric (cpu, mem, netUp, netDown, online, load1/5/15). The
// SystemHistoryModal calls one endpoint per active tab.
func (a *ServerController) getMetricHistoryBucket(c *gin.Context) {
	metric := c.Param("metric")
	if !slices.Contains(service.SystemMetricKeys, metric) {
		jsonMsg(c, "invalid metric", fmt.Errorf("unknown metric"))
		return
	}
	bucket, ok := parseHistoryBucket(c)
	if !ok {
		return
	}
	jsonObj(c, a.serverService.AggregateSystemMetric(metric, bucket, 60), nil)
}

func (a *ServerController) getXrayMetricsState(c *gin.Context) {
	jsonObj(c, a.xrayMetricsService.State(), nil)
}

func (a *ServerController) getXrayMetricsHistoryBucket(c *gin.Context) {
	metric := c.Param("metric")
	if !slices.Contains(service.XrayMetricKeys, metric) {
		jsonMsg(c, "invalid metric", fmt.Errorf("unknown metric"))
		return

View on GitHub (pinned to ad32144c42)

Solutions

  1. Use exactly one of: cpu, mem, netUp, netDown, online, load1, load5, load15 (confirm via service.SystemMetricKeys in your build).
  2. Match casing — netUp not net_up.
  3. Upgrade frontend and backend together so the SystemHistoryModal tabs align with the allowlist.

Example fix

# before
GET /panel/api/server/history/metric/memory/60   # invalid metric

# after
GET /panel/api/server/history/metric/mem/60
Defensive patterns

Strategy: type-guard

Validate before calling

const SYSTEM_METRIC_KEYS = ['cpu','mem','netUp','netDown','online','load1','load5','load15'] as const
function isSystemMetric(m: string): m is (typeof SYSTEM_METRIC_KEYS)[number] {
  return (SYSTEM_METRIC_KEYS as readonly string[]).includes(m)
}

Type guard

function isSystemMetric(m: string): m is (typeof SYSTEM_METRIC_KEYS)[number] {
  return (SYSTEM_METRIC_KEYS as readonly string[]).includes(m)
}

Prevention

When it happens

Trigger: GET /panel/api/server/history/metric/:metric/:bucket with e.g. 'memory' (key is 'mem'), 'net_up' (key is 'netUp'), 'load' (must be load1/load5/load15), or any invented key.

Common situations: Guessing snake_case names when the API uses camelCase; frontend/backend drift after partial upgrade; copying metric names from the node endpoint when the sets differ.

Related errors


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