{"record":{"id":"b765e5aef4cf3aa7","repo":"Billionmail/BillionMail","slug":"retention-days-must-be-a-number","errorCode":null,"errorMessage":"retention_days must be a number","messagePattern":"retention_days must be a number","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/controller/settings/settings.go","lineNumber":239,"sourceCode":"\t\tif !public.IsValidCIDR(value) {\n\t\t\treturn fmt.Errorf(\"IPv4 network format is incorrect, please use CIDR format (e.g. 192.168.1.0/24)\")\n\t\t}\n\n\tcase \"TZ\", \"timezone\":\n\t\t// Timezone: check if it is a valid timezone\n\t\tif !public.IsValidTimezone(value) {\n\t\t\treturn fmt.Errorf(\"invalid timezone\")\n\t\t}\n\n\tcase \"FAIL2BAN_INIT\", \"fail2ban\":\n\t\t// fail2ban: only allowed y/n or 1/0\n\t\tif value != \"y\" && value != \"n\" && value != \"1\" && value != \"0\" {\n\t\t\treturn fmt.Errorf(\"fail2ban value can only be y/n or 1/0\")\n\t\t}\n\tcase \"RETENTION_DAYS\", \"retention_days\":\n\t\t// retention_days: must be a number\n\t\tif _, err := strconv.Atoi(value); err != nil {\n\t\t\treturn fmt.Errorf(\"retention_days must be a number\")\n\t\t}\n\t}\n\n\t// General character check: not allowed dangerous characters\n\tif public.ContainsDangerousChars(value) {\n\t\treturn fmt.Errorf(\"configuration value contains illegal characters\")\n\t}\n\n\treturn nil\n}\n","sourceCodeStart":221,"sourceCodeEnd":250,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/controller/settings/settings.go#L221-L250","documentation":"RETENTION_DAYS must parse as an integer via strconv.Atoi; validateConfigValue rejects it otherwise. This value controls how long maillog/statistics data is retained, and a non-numeric value would break downstream numeric comparisons and cleanup jobs.","triggerScenarios":"Setting RETENTION_DAYS to a non-numeric string such as '30d', 'thirty', '', '30 days', or a value with a decimal point like '30.5'.","commonSituations":"Users append units ('30d', '1 month') or paste values with whitespace; frontends send formatted numbers with thousands separators ('1,000').","solutions":["Send a plain integer string, e.g. '30'","Strip units and separators before the call ('30 days'→'30')","Use whole days only — decimals are rejected by Atoi","Ensure the field is not empty when the form is submitted"],"exampleFix":"// before\nRETENTION_DAYS = 30d\n// after\nRETENTION_DAYS = 30","handlingStrategy":"validation","validationCode":"const n = Number(retentionDays);\nif (!Number.isInteger(n) || n <= 0) throw new Error('retention_days must be a positive integer');","typeGuard":"function isWholeDays(v: unknown): v is number {\n  return typeof v === 'number' && Number.isInteger(v) && v > 0;\n}","tryCatchPattern":"try {\n  await api.setSystemConfigKey('RETENTION_DAYS', String(days));\n} catch (e) {\n  if (String(e.message).includes('retention_days')) {\n    notify('Enter retention as a plain number of days, e.g. 30');\n  } else throw e;\n}","preventionTips":["Send whole days as a plain integer, no units or decimals","Strip thousands separators and whitespace","Use a number input, not a text field","Validate > 0 client-side"],"tags":["validation","config","numeric"],"backgroundTag":"invalid-numeric-input","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}