{"record":{"id":"0fe29e4cb9354315","repo":"chenhg5/cc-connect","slug":"empty-delay-or-time","errorCode":null,"errorMessage":"empty delay or time","messagePattern":"empty delay or time","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/timer.go","lineNumber":451,"sourceCode":"\t\tslog.Info(\"timer: job completed\", \"id\", jobID)\n\t}\n}\n\nfunc GenerateTimerID() string {\n\tb := make([]byte, 4)\n\tif _, err := rand.Read(b); err != nil {\n\t\tpanic(fmt.Errorf(\"generate timer id: %w\", err))\n\t}\n\treturn hex.EncodeToString(b)\n}\n\n// ParseDelayOrTime parses a relative duration (\"2h\", \"30m\", \"1h30m\") or\n// an absolute ISO time (\"2026-05-15T14:00\", \"2026-05-15T14:00:00+08:00\")\n// and returns the absolute fire time.\nfunc ParseDelayOrTime(s string) (time.Time, error) {\n\ts = strings.TrimSpace(s)\n\tif s == \"\" {\n\t\treturn time.Time{}, fmt.Errorf(\"empty delay or time\")\n\t}\n\n\t// Try as a Go duration first (e.g., \"2h\", \"30m\", \"1h30m\", \"2h30m15s\")\n\tif d, err := time.ParseDuration(s); err == nil {\n\t\tif d <= 0 {\n\t\t\treturn time.Time{}, fmt.Errorf(\"delay must be positive\")\n\t\t}\n\t\treturn time.Now().Add(d), nil\n\t}\n\n\t// Try ISO time formats\n\t// RFC3339 includes timezone (e.g. \"2026-05-15T14:00:00+08:00\"),\n\t// so it's parsed directly. The other layouts have no timezone\n\t// and are interpreted in the system's local timezone.\n\tlayouts := []struct {\n\t\tlayout string\n\t\tlocal  bool\n\t}{","sourceCodeStart":433,"sourceCodeEnd":469,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/core/timer.go#L433-L469","documentation":"ParseDelayOrTime in core/timer.go parses a relative Go duration or an absolute ISO time for a timer. It first trims the input; if the result is an empty string there is nothing to parse, so it immediately returns this error instead of attempting any format detection. It is a guard against callers passing blank scheduling input.","triggerScenarios":"Calling ParseDelayOrTime(\"\") or ParseDelayOrTime(\"   \") — any string that is empty or whitespace-only after strings.TrimSpace. Reachable via timer add commands (handleTimerAdd, cmdTimerAdd, cmdTimerAddExec) when the user supplies no delay/time argument.","commonSituations":"User runs the timer add command with a missing argument (e.g. '/timer add' with no value); a bot integration forwards an unset or stripped field; a shell expansion or variable interpolation produced an empty string.","solutions":["Provide a non-empty value: a positive duration like '2h30m' or an ISO time like '2026-05-15T14:00'.","In command handlers, check the argument for emptiness before calling ParseDelayOrTime and reply with usage text.","If the value comes from user input, trim whitespace and reject blank input upstream with a friendlier message."],"exampleFix":"// before\ntm, err := core.ParseDelayOrTime(arg)\n// after\nif strings.TrimSpace(arg) == \"\" {\n    return fmt.Errorf(\"usage: /timer add <2h30m | 2026-05-15T14:00>\")\n}\ntm, err := core.ParseDelayOrTime(arg)","handlingStrategy":"validation","validationCode":"if strings.TrimSpace(arg) == \"\" {\n    return fmt.Errorf(\"timer: delay or time is required (e.g. 2h30m or 2026-05-15T14:00)\")\n}\n_, err := core.ParseDelayOrTime(arg)","typeGuard":null,"tryCatchPattern":"if _, err := core.ParseDelayOrTime(arg); err != nil {\n    if err.Error() == \"empty delay or time\" {\n        reply(\"Usage: /timer add <duration|ISO-time>\")\n        return\n    }\n    reply(\"Invalid timer: \" + err.Error())\n}","preventionTips":["Validate command arguments for emptiness before parsing.","Always show usage text when a required argument is missing.","Trim user input before passing it on."],"tags":["timer","validation","empty-input"],"backgroundTag":"empty-required-field","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}