{"record":{"id":"f81967d9f061eb6f","repo":"hashicorp/nomad","slug":"failed-parsing-cron-expression-q","errorCode":null,"errorMessage":"failed parsing cron expression: %q","messagePattern":"failed parsing cron expression: %q","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"api/jobs.go","lineNumber":960,"sourceCode":"\t\tif err != nil {\n\t\t\treturn time.Time{}, fmt.Errorf(\"failed parsing cron expression %s: %v\", spec, err)\n\t\t}\n\t\tif nextTime.IsZero() || t.Before(nextTime) {\n\t\t\tnextTime = t\n\t\t}\n\t}\n\treturn nextTime, nil\n}\n\n// cronParseNext is a helper that parses the next time for the given expression\n// but captures any panic that may occur in the underlying library.\n// ---  THIS FUNCTION IS REPLICATED IN nomad/structs/structs.go\n// and should be kept in sync.\nfunc cronParseNext(fromTime time.Time, spec string) (t time.Time, err error) {\n\tdefer func() {\n\t\tif recover() != nil {\n\t\t\tt = time.Time{}\n\t\t\terr = fmt.Errorf(\"failed parsing cron expression: %q\", spec)\n\t\t}\n\t}()\n\texp, err := cronexpr.Parse(spec)\n\tif err != nil {\n\t\treturn time.Time{}, fmt.Errorf(\"failed parsing cron expression: %s: %v\", spec, err)\n\t}\n\treturn exp.Next(fromTime), nil\n}\n\nfunc (p *PeriodicConfig) GetLocation() (*time.Location, error) {\n\tif p.TimeZone == nil || *p.TimeZone == \"\" {\n\t\treturn time.UTC, nil\n\t}\n\n\treturn time.LoadLocation(*p.TimeZone)\n}\n\n// ParameterizedJobConfig is used to configure the parameterized job.","sourceCodeStart":942,"sourceCodeEnd":978,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/api/jobs.go#L942-L978","documentation":"cronParseNext recovers panics from the cronexpr library and converts them into this error ('failed parsing cron expression: %q'), quoting the whole spec. cronexpr is known to panic on some malformed inputs, so the deferred recover guards callers of PeriodicConfig.Next from crashing. This copy lives in api/jobs.go and is replicated in nomad/structs.","triggerScenarios":"A cron spec causes cronexpr.Parse or exp.Next to panic — typically deeply malformed specs, e.g. out-of-range values or pathological expressions.","commonSituations":"User-supplied periodic job specs from untrusted config; specs mutated programmatically; specs that pass Parse but panic on Next in certain cronexpr versions.","solutions":["Sanitize/validate the spec string before storing it in the job config","Test the spec against cronexpr.Parse in isolation to reproduce the panic","Upgrade Nomad/cronexpr for panic fixes","Fix the malformed field values (day-of-month 0, month 13, etc.)"],"exampleFix":"// before\nspec = \"* * * 13 * *\" // month 13 → panic in cronexpr\n// after\nspec = \"* * * 12 * *\"","handlingStrategy":"validation","validationCode":"func validateCronSpecSafe(spec string) (ok bool) {\n    defer func() { _ = recover() }()\n    exp, err := cronexpr.Parse(spec)\n    if err != nil {\n        return false\n    }\n    exp.Next(time.Now())\n    return true\n}","typeGuard":null,"tryCatchPattern":"t, err := cronParseNext(time.Now(), spec)\nif err != nil && strings.HasPrefix(err.Error(), \"failed parsing cron expression\") {\n    return ErrBadPeriodicConfig\n}","preventionTips":["Run spec validation through a panic-guarded parser at config load","Reject user-supplied specs before they reach job registration","Test unusual specs against the exact cronexpr version Nomad embeds"],"tags":["cron","panic-recovery","periodic-jobs"],"backgroundTag":"invalid-cron-expression","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}