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
- Send durations as strings ("30s", "5m") or numbers (nanoseconds), or omit the field entirely.
- Fix the producer to skip null fields (e.g. JSON.stringify drops undefined, not null).
- 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
- Always send durations as strings ("30s") or omit the field; never serialize null for optional durations.
- In JS, use undefined (not null) for unset fields so JSON.stringify drops them.
- Add API contract tests that assert the JSON type of every duration field.
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
- router not part of network
- peer and peer_groups cannot be set at the same time
- either peer or peer_groups must be provided
- posture checks name shouldn't be empty
- posture checks shouldn't be empty
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/a8cabc4c7935d109.
Report an issue: GitHub.