{"record":{"id":"6d05c4408a8e226b","repo":"MHSanaei/3x-ui","slug":"invalid-bucket","errorCode":null,"errorMessage":"invalid bucket","messagePattern":"invalid bucket","errorType":"http","errorClass":null,"httpStatus":200,"severity":"error","filePath":"internal/web/controller/node.go","lineNumber":354,"sourceCode":"\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":336,"sourceCodeEnd":359,"githubUrl":"https://github.com/MHSanaei/3x-ui/blob/ad32144c42455696ea9f14e12168beac3e25f5d2/internal/web/controller/node.go#L336-L359","documentation":"NodeController.history converts c.Param(\"bucket\") to an int and requires it to be positive AND in service.IsAllowedHistoryBucket's allowed set; otherwise it returns 'invalid bucket' with detail 'unsupported bucket'. Buckets are pre-defined aggregation window sizes (in seconds), not arbitrary numbers, so a numerically valid value can still be rejected.","triggerScenarios":"GET /panel/api/nodes/history/:id/:metric/:bucket with bucket=0, a negative number, a non-integer ('1h'), or an integer outside the allowed set (e.g. 37 when only 60/300/… are allowed).","commonSituations":"Passing minutes instead of seconds; UI dropdown desynced from backend after upgrade; hand-written dashboards guessing the bucket size.","solutions":["Use an allowed bucket value — check service.IsAllowedHistoryBucket in your build for the exact set (commonly 60, 300, 900, 3600, …).","Pass the value as a plain integer path segment, no units.","After upgrading the panel, reload the frontend so its bucket selector uses the current set."],"exampleFix":"# before\nGET /panel/api/nodes/history/1/cpu/37     # not an allowed bucket\nGET /panel/api/nodes/history/1/cpu/1h    # not an integer\n\n# after\nGET /panel/api/nodes/history/1/cpu/60","handlingStrategy":"validation","validationCode":"const ALLOWED_BUCKETS = [60, 300, 900, 3600] // mirror service.IsAllowedHistoryBucket\nif (!ALLOWED_BUCKETS.includes(bucket)) throw new Error(`bucket must be one of ${ALLOWED_BUCKETS}`)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Populate the bucket selector from the allowed set, not free entry.","Pass seconds as plain integers, no units or strings.","Re-verify the allowed set after each panel upgrade."],"tags":["api","http-400","validation","metrics","nodes"],"backgroundTag":null,"analyzedSha":"ad32144c42455696ea9f14e12168beac3e25f5d2","analyzedAt":"2026-08-15T11:13:23.905Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}