crowdsecurity/crowdsec · error · ParseDurationFail

decision duration '%+v': %w: %w

Error message

decision duration '%+v': %w: %w

What it means

CrowdSec failed to parse the `duration` field of a decision being persisted to the database. `cstime.ParseDurationWithDays` accepts Go duration strings (e.g. '4h', '30m') plus a 'd' suffix for days (e.g. '4d'); anything outside that grammar fails and the whole decision batch is rejected. The decision is not saved and the error aborts createDecisionBatch with the ParseDurationFail sentinel attached.

Source

Thrown at pkg/database/alerts.go:377

	log.Debugf("deleted %d decisions for %s vs %s", deleted, decOrigin, *alertItem.Decisions[0].Origin)

	err = txClient.Commit()
	if err != nil {
		return 0, 0, 0, rollbackOnError(txClient, err, "error committing transaction")
	}

	return alertRef.ID, inserted, deleted, nil
}

func (c *Client) createDecisionBatch(ctx context.Context, client *ent.Client, simulated bool, stopAtTime time.Time, decisions []*models.Decision) ([]*ent.Decision, error) {
	decisionCreate := []*ent.DecisionCreate{}

	for _, decisionItem := range decisions {
		var rng csnet.Range

		duration, err := cstime.ParseDurationWithDays(*decisionItem.Duration)
		if err != nil {
			return nil, fmt.Errorf("decision duration '%+v': %w: %w", *decisionItem.Duration, err, ParseDurationFail)
		}

		// if the scope is IP or Range, convert the value to integers
		if strings.ToLower(*decisionItem.Scope) == "ip" || strings.ToLower(*decisionItem.Scope) == "range" {
			rng, err = csnet.NewRange(*decisionItem.Value)
			if err != nil {
				c.Log.Errorf("invalid addr/range '%s': %s", *decisionItem.Value, err)
				continue
			}
		}

		newDecision := client.Decision.Create().
			SetUntil(stopAtTime.Add(duration)).
			SetScenario(*decisionItem.Scenario).
			SetType(*decisionItem.Type).
			SetStartIP(rng.Start.Addr).
			SetStartSuffix(rng.Start.Sfx).
			SetEndIP(rng.End.Addr).

View on GitHub (pinned to 909b515798)

Solutions

  1. Fix the decision's Duration value to a valid Go duration or day-suffixed string ('4h', '30m', '7d') before inserting the alert
  2. Validate durations with cstime.ParseDurationWithDays in the producer before pushing to LAPI
  3. If duration may be absent, leave the field nil/empty rather than sending an unparseable placeholder string
  4. Inspect the quoted value in the error message to see the exact malformed input and its source (origin LAPI/parser/bouncer)

Example fix

// before
decision := &models.Decision{Duration: ptr("2 weeks"), ...}
// after
decision := &models.Decision{Duration: ptr("336h"), ...} // or "14d"
Defensive patterns

Strategy: validation

Validate before calling

import "github.com/crowdsecurity/crowdsec/pkg/cstime"
if _, err := cstime.ParseDurationWithDays(dur); err != nil {
    return fmt.Errorf("invalid decision duration %q: %w", dur, err)
}

Try / catch

var parseErr *database.ParseDurationFail
if errors.As(err, &parseErr) {
    log.Warnf("skipping decision with bad duration: %s", err)
}

Prevention

When it happens

Trigger: Calling Alert.Create / alert insertion (LAPI push, cscli decisions add, bouncer feed) with a decision whose Duration pointer is non-nil but contains an unparseable string, e.g. 'forever', '', '2w', '1 y', or a float like '4.5h' when the parser doesn't accept it.

Common situations: Custom parsers/notifications or bouncers pushing hand-built decisions with human-written durations; scripts posting to LAPI with duration spelled differently than Go time.ParseDuration syntax; upstream data format changes (duration given as number of seconds instead of a duration string).

Understand the failure class

Background: "invalid duration" / "failed to parse duration": why your timeout, interval, or TTL string is rejected and which formats each library accepts — this error's family across 32 libraries.

Related errors


AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06). Data as JSON: /api/errors/6c7980a2840eb1b5. Report an issue: GitHub.