{"record":{"id":"540b8dbfba7646ac","repo":"MHSanaei/3x-ui","slug":"invalid-metric-540b8d","errorCode":null,"errorMessage":"invalid metric","messagePattern":"invalid metric","errorType":"http","errorClass":null,"httpStatus":200,"severity":"error","filePath":"internal/web/controller/server.go","lineNumber":140,"sourceCode":"\n// getCpuHistoryBucket retrieves aggregated CPU usage history based on the specified time bucket.\n// Kept for back-compat; new callers should use /history/cpu/:bucket which\n// returns {\"t\",\"v\"} (uniform across all metrics) instead of {\"t\",\"cpu\"}.\nfunc (a *ServerController) getCpuHistoryBucket(c *gin.Context) {\n\tbucket, ok := parseHistoryBucket(c)\n\tif !ok {\n\t\treturn\n\t}\n\tjsonObj(c, a.serverService.AggregateCpuHistory(bucket, 60), nil)\n}\n\n// getMetricHistoryBucket returns up to 60 buckets of history for a single\n// system metric (cpu, mem, netUp, netDown, online, load1/5/15). The\n// SystemHistoryModal calls one endpoint per active tab.\nfunc (a *ServerController) getMetricHistoryBucket(c *gin.Context) {\n\tmetric := c.Param(\"metric\")\n\tif !slices.Contains(service.SystemMetricKeys, metric) {\n\t\tjsonMsg(c, \"invalid metric\", fmt.Errorf(\"unknown metric\"))\n\t\treturn\n\t}\n\tbucket, ok := parseHistoryBucket(c)\n\tif !ok {\n\t\treturn\n\t}\n\tjsonObj(c, a.serverService.AggregateSystemMetric(metric, bucket, 60), nil)\n}\n\nfunc (a *ServerController) getXrayMetricsState(c *gin.Context) {\n\tjsonObj(c, a.xrayMetricsService.State(), nil)\n}\n\nfunc (a *ServerController) getXrayMetricsHistoryBucket(c *gin.Context) {\n\tmetric := c.Param(\"metric\")\n\tif !slices.Contains(service.XrayMetricKeys, metric) {\n\t\tjsonMsg(c, \"invalid metric\", fmt.Errorf(\"unknown metric\"))\n\t\treturn","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/MHSanaei/3x-ui/blob/ad32144c42455696ea9f14e12168beac3e25f5d2/internal/web/controller/server.go#L122-L158","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use exactly one of: cpu, mem, netUp, netDown, online, load1, load5, load15 (confirm via service.SystemMetricKeys in your build).","Match casing — netUp not net_up.","Upgrade frontend and backend together so the SystemHistoryModal tabs align with the allowlist."],"exampleFix":"# before\nGET /panel/api/server/history/metric/memory/60   # invalid metric\n\n# after\nGET /panel/api/server/history/metric/mem/60","handlingStrategy":"type-guard","validationCode":"const SYSTEM_METRIC_KEYS = ['cpu','mem','netUp','netDown','online','load1','load5','load15'] as const\nfunction isSystemMetric(m: string): m is (typeof SYSTEM_METRIC_KEYS)[number] {\n  return (SYSTEM_METRIC_KEYS as readonly string[]).includes(m)\n}","typeGuard":"function isSystemMetric(m: string): m is (typeof SYSTEM_METRIC_KEYS)[number] {\n  return (SYSTEM_METRIC_KEYS as readonly string[]).includes(m)\n}","tryCatchPattern":null,"preventionTips":["Use the exact camelCase keys; do not snake_case them.","Keep the metric list in a generated/shared module so renaming is caught at compile time.","Distinguish system-metric keys from xray-metric keys per endpoint."],"tags":["api","http-400","validation","metrics"],"backgroundTag":null,"analyzedSha":"ad32144c42455696ea9f14e12168beac3e25f5d2","analyzedAt":"2026-08-15T11:13:23.905Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}