goharbor/harbor · error · errors.Error

BAD_REQUEST

BAD_REQUEST

Error message

invalid cron string for scheduled preheat: %s, error: %v

What it means

Schema.ValidatePreheatPolicy rejects a P2P preheat policy whose trigger is scheduled but whose cron string is invalid. Harbor parses cron with a 6-field parser (second minute hour day-of-month month day-of-week) and additionally requires the first field (seconds) to be the literal 0 — so both malformed strings and non-zero seconds fail. Maps to BAD_REQUEST when creating/updating policies (POST/PUT /api/v2.0/projects/{project}/preheat/policies).

Source

Thrown at src/pkg/p2p/preheat/models/policy/policy.go:126

// TriggerType represents the type of trigger.
type TriggerType = string

// Trigger holds the trigger info.
type Trigger struct {
	// The preheat policy trigger type. The valid values ar manual, scheduled.
	Type     TriggerType `json:"type"`
	Settings struct {
		// The cron string for scheduled trigger.
		Cron string `json:"cron,omitempty"`
	} `json:"trigger_setting,omitempty"`
}

// ValidatePreheatPolicy validate preheat policy
func (s *Schema) ValidatePreheatPolicy() error {
	// currently only validate cron string of preheat policy
	if s.Trigger != nil && s.Trigger.Type == TriggerTypeScheduled && len(s.Trigger.Settings.Cron) > 0 {
		if err := utils.ValidateCronString(s.Trigger.Settings.Cron); err != nil {
			return errors.New(nil).WithCode(errors.BadRequestCode).
				WithMessagef("invalid cron string for scheduled preheat: %s, error: %v", s.Trigger.Settings.Cron, err)
		}
	}

	return nil
}

// Encode encodes policy schema.
func (s *Schema) Encode() error {
	if s.Filters != nil {
		filterStr, err := json.Marshal(s.Filters)
		if err != nil {
			return err
		}
		s.FiltersStr = string(filterStr)
	}

	if s.Trigger != nil {

View on GitHub (pinned to 7b2fd08cc5)

Solutions

  1. Use exactly 6 fields with a leading 0 for seconds, e.g. "0 */5 * * * *" fires every 5 minutes.
  2. Validate the string with the same rules client-side: 6 fields, parseable, first field == "0".
  3. If the policy only needs manual or event-based preheat, use trigger type "manual"/"event" instead of "scheduled".

Example fix

# before
curl -X POST https://harbor/api/v2.0/projects/1/preheat/policies -d '{"name": "p", "provider_id": 1, "trigger": {"type": "scheduled", "trigger_setting": {"cron": "*/5 * * * *"}}}'
# after
curl -X POST https://harbor/api/v2.0/projects/1/preheat/policies -d '{"name": "p", "provider_id": 1, "trigger": {"type": "scheduled", "trigger_setting": {"cron": "0 */5 * * * *"}}}'
Defensive patterns

Strategy: validation

Validate before calling

// Replicate Harbor's cron rules before submitting a scheduled policy
func validPreheatCron(c string) error {
    if len(c) == 0 { return errors.New("cron string is required") }
    if _, err := utils.CronParser().Parse(c); err != nil { return err } // 6 fields: sec min hour dom mon dow
    p := strings.Split(c, " ")
    if len(p) == 6 && p[0] != "0" { return errors.New("seconds field must be 0") }
    return nil
}

Type guard

function isScheduledPolicyWithBadCron(p) {
  return p?.trigger?.type === 'scheduled' && !validCron6(p.trigger.trigger_setting?.cron);
}

Try / catch

On HTTP 400 'invalid cron string for scheduled preheat', parse the returned error detail, coerce the cron to 6 fields with seconds=0, and resubmit the policy once.

Prevention

When it happens

Trigger: POST/PUT preheat policy with trigger {"type": "scheduled", "trigger_setting": {"cron": "*/5 * * * *"}} — a classic 5-field crontab string; or 6 fields with a non-zero seconds field like "30 */5 * * * *"; or invalid ranges/step syntax per robfig/cron.

Common situations: Copying standard 5-field crontab entries from Linux docs; assuming Harbor uses the same cron dialect as Dragonfly/Kraken; harbor replicate-schedule muscle memory where seconds are allowed; step values on day-of-week names.

Related errors


AI-assisted analysis of goharbor/harbor@7b2fd08cc5 (2026-08-16). Data as JSON: /api/errors/717f7fe506299b06. Report an issue: GitHub.