{"record":{"id":"a5bc0f567096134f","repo":"AdguardTeam/AdGuardHome","slug":"s-must-be-a-multiple-of-1-hour","errorCode":null,"errorMessage":"%s: must be a multiple of 1 hour","messagePattern":"(.+?): must be a multiple of 1 hour","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/stats/http.go","lineNumber":118,"sourceCode":"// the parameter is empty, the original limit is returned.\nfunc parseRecent(recent string, limit time.Duration) (parsedLimit time.Duration, err error) {\n\tif recent == \"\" {\n\t\treturn limit, nil\n\t}\n\n\trecentMs, err := strconv.ParseInt(recent, 10, 64)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"%s: parsing interval: %s\", queryKeyRecent, err)\n\t}\n\n\terr = validate.InRange(queryKeyRecent, recentMs, millisecondsInHour, limit.Milliseconds())\n\tif err != nil {\n\t\t// Don't wrap the error since it's already informative enough as is.\n\t\treturn 0, err\n\t}\n\n\tif recentMs%millisecondsInHour != 0 {\n\t\treturn 0, fmt.Errorf(\"%s: must be a multiple of 1 hour\", queryKeyRecent)\n\t}\n\n\treturn time.Duration(recentMs) * time.Millisecond, nil\n}\n\n// configResp is the response to the GET /control/stats_info.\ntype configResp struct {\n\tIntervalDays uint32 `json:\"interval\"`\n}\n\n// getConfigResp is the response to the GET /control/stats_info.\ntype getConfigResp struct {\n\t// Ignored is the list of host names, which should not be counted.\n\tIgnored []string `json:\"ignored\"`\n\n\t// Interval is the statistics rotation interval in milliseconds.\n\tInterval float64 `json:\"interval\"`\n","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/AdguardTeam/AdGuardHome/blob/b41aefbe51c8dde65e2c50f093996afa0502edf9/internal/stats/http.go#L100-L136","documentation":"The stats HTTP handler requires the 'recent' query parameter (milliseconds) to be a whole number of hours. This error is returned when recentMs % 3600000 != 0.","triggerScenarios":"Calling /control/stats with a recent value that is not a multiple of one hour in milliseconds, e.g. recent=1800000 (30 minutes) or recent=1.","commonSituations":"Clients computing recent from arbitrary durations (minutes, seconds) or passing 1 meaning '1 hour'; stale clients from before the hourly constraint was added.","solutions":["Round the desired duration up/down to whole hours before sending","Send e.g. recent=3600000 for 1 hour","Omit recent to use the configured default limit"],"exampleFix":"// before\nrecent := 30 * time.Minute\n// after\nrecent := 1 * time.Hour","handlingStrategy":"validation","validationCode":"ms := d.Milliseconds()\nms = ms / 3600000 * 3600000 // round down to whole hours\nif ms <= 0 { ms = 3600000 }","typeGuard":"func isWholeHours(ms int64) bool { return ms%3600000 == 0 }","tryCatchPattern":null,"preventionTips":["Snap durations to whole hours client-side","Document the hourly constraint in client wrappers"],"tags":["http","query-param","validation","stats"],"backgroundTag":"invalid-query-parameter","analyzedSha":"b41aefbe51c8dde65e2c50f093996afa0502edf9","analyzedAt":"2026-08-27T04:57:55.097Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}