netbirdio/netbird · error

invalid duration

Error message

invalid duration

What it means

util.Duration is a JSON-only duration wrapper. UnmarshalJSON first decodes the raw JSON into interface{} and switches on the type: float64 is taken as nanoseconds, string is parsed with time.ParseDuration; every other JSON type (bool, null, array, object) falls to the default branch and returns this generic error. Note that a malformed string such as "5x" returns the time.ParseDuration error instead, not this one.

Source

Thrown at util/duration.go:35

func (d *Duration) UnmarshalJSON(b []byte) error {
	var v interface{}
	if err := json.Unmarshal(b, &v); err != nil {
		return err
	}
	switch value := v.(type) {
	case float64:
		d.Duration = time.Duration(value)
		return nil
	case string:
		var err error
		d.Duration, err = time.ParseDuration(value)
		if err != nil {
			return err
		}
		return nil
	default:
		return errors.New("invalid duration")
	}
}

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Send durations as strings ("30s", "5m") or numbers (nanoseconds), or omit the field entirely.
  2. Fix the producer to skip null fields (e.g. JSON.stringify drops undefined, not null).
  3. If null must be tolerated, decode into a custom nullable type or a map first and only call UnmarshalJSON for present, non-null values.

Example fix

// before
{"pollInterval": null}

// after
{"pollInterval": "30s"}
// or omit the key entirely
Defensive patterns

Strategy: validation

Validate before calling

// Validate payload shape before unmarshalling into util.Duration fields
func isValidDurationJSON(raw []byte) bool {
    var v any
    if err := json.Unmarshal(raw, &v); err != nil {
        return false
    }
    switch v.(type) {
    case float64, string:
        return true
    }
    return false
}

Type guard

func isInvalidDuration(err error) bool {
    return err != nil && err.Error() == "invalid duration"
}

Try / catch

if err := json.Unmarshal(data, &cfg); err != nil {
    if isInvalidDuration(err) {
        // payload had a null/bool/array/object where a duration belongs:
        // fix the producer to send "30s" or omit the field
    }
    return err
}

Prevention

When it happens

Trigger: Sending {"timeout": null} (or true, [], {}) to an API field typed util.Duration; a frontend serializing an unset optional field as null instead of omitting it; hand-built JSON with the wrong type.

Common situations: JS clients where an undefined field becomes null in JSON.stringify; OpenAPI generators emitting null for optional durations; a boolean mistakenly serialized into a duration field.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/a8cabc4c7935d109. Report an issue: GitHub.