{"record":{"id":"d54fdad4a79a825e","repo":"MHSanaei/3x-ui","slug":"invalid-metric","errorCode":null,"errorMessage":"invalid metric","messagePattern":"invalid metric","errorType":"http","errorClass":null,"httpStatus":200,"severity":"error","filePath":"internal/web/controller/node.go","lineNumber":349,"sourceCode":"\t\treturn\n\t}\n\tif len(req.Ids) == 0 {\n\t\tjsonMsg(c, I18nWeb(c, \"somethingWentWrong\"), fmt.Errorf(\"no nodes selected\"))\n\t\treturn\n\t}\n\tresults, err := a.nodeService.UpdatePanels(req.Ids, req.Dev)\n\tjsonMsgObj(c, I18nWeb(c, \"pages.nodes.toasts.updateStarted\"), results, err)\n}\n\nfunc (a *NodeController) history(c *gin.Context) {\n\tid, err := strconv.Atoi(c.Param(\"id\"))\n\tif err != nil {\n\t\tjsonMsg(c, I18nWeb(c, \"get\"), err)\n\t\treturn\n\t}\n\tmetric := c.Param(\"metric\")\n\tif !slices.Contains(service.NodeMetricKeys, metric) {\n\t\tjsonMsg(c, \"invalid metric\", fmt.Errorf(\"unknown metric\"))\n\t\treturn\n\t}\n\tbucket, err := strconv.Atoi(c.Param(\"bucket\"))\n\tif err != nil || bucket <= 0 || !service.IsAllowedHistoryBucket(bucket) {\n\t\tjsonMsg(c, \"invalid bucket\", fmt.Errorf(\"unsupported bucket\"))\n\t\treturn\n\t}\n\tjsonObj(c, a.nodeService.AggregateNodeMetric(id, metric, bucket, 60), nil)\n}\n","sourceCodeStart":331,"sourceCodeEnd":359,"githubUrl":"https://github.com/MHSanaei/3x-ui/blob/ad32144c42455696ea9f14e12168beac3e25f5d2/internal/web/controller/node.go#L331-L359","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nGET /panel/api/nodes/history/1/cpuPercent/60   # invalid metric\n\n# after\nGET /panel/api/nodes/history/1/cpu/60","handlingStrategy":"type-guard","validationCode":"// derive allowed list from the API contract, not guesses\nconst NODE_METRIC_KEYS = ['cpu', 'mem', 'netUp', 'netDown'] as const\ntype NodeMetric = (typeof NODE_METRIC_KEYS)[number]\nfunction isNodeMetric(m: string): m is NodeMetric {\n  return (NODE_METRIC_KEYS as readonly string[]).includes(m)\n}","typeGuard":"function isNodeMetric(m: string): m is NodeMetric {\n  return (NODE_METRIC_KEYS as readonly string[]).includes(m)\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["api","http-400","validation","metrics","nodes"],"backgroundTag":null,"analyzedSha":"ad32144c42455696ea9f14e12168beac3e25f5d2","analyzedAt":"2026-08-15T11:13:23.905Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}