netbirdio/netbird · warning · ErrDeadlineBeforeEpoch

%w: %v

Error message

%w: %v

What it means

sessionwatch.Watcher.Update rejected the SSO session expiry deadline published by management because it pre-dates the Unix epoch (before 1970-01-01). The watcher first clears its own state and notifies the recorder, then returns the sentinel ErrDeadlineBeforeEpoch wrapped with the offending timestamp. It signals a malformed timestamp on the wire rather than a real session deadline, and is designed for errors.Is matching.

Source

Thrown at client/internal/auth/sessionwatch/watcher.go:160

// applySessionDeadline forces a zero deadline into the status recorder
// after a non-nil error).
func (w *Watcher) Update(deadline time.Time) error {
	w.mu.Lock()
	if w.closed {
		w.mu.Unlock()
		return nil
	}

	if deadline.IsZero() {
		w.clearLocked()
		return nil
	}

	now := time.Now()
	switch {
	case deadline.Before(time.Unix(0, 0)):
		w.clearLocked()
		return fmt.Errorf("%w: %v", ErrDeadlineBeforeEpoch, deadline)
	case deadline.After(now.Add(maxDeadlineHorizon)):
		w.clearLocked()
		return fmt.Errorf("%w: %v", ErrDeadlineTooFarFuture, deadline)
	case deadline.Before(now.Add(-maxPastHorizon)):
		w.clearLocked()
		return fmt.Errorf("%w: %v (now=%v)", ErrDeadlineInPast, deadline, now)
	}

	if deadline.Equal(w.current) {
		w.mu.Unlock()
		return nil
	}

	w.stopTimerLocked()
	w.current = deadline
	// Reset every per-deadline guard so a refreshed deadline arms a fresh
	// warning cycle: both edge triggers and the user Dismiss decision
	// (the user agreed to the old deadline expiring; a new deadline

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Handle it as 'no deadline': the watcher already cleared its state, so push a zero deadline into any other sinks (applySessionDeadline does exactly this on error).
  2. Report the agent and management versions together with the rejected value printed after the colon - the timestamp is malformed on the wire and needs a server-side fix.
  3. Upgrade the management service if it is older than the session-expiry feature; a current server omits the field instead of sending a bogus value.
  4. If self-hosted, inspect the LoginResponse/SyncResponse the server actually emits for the expiry field.
Defensive patterns

Strategy: type-guard

Validate before calling

// validate a session deadline before feeding it to the watcher
func validSessionDeadline(d time.Time) bool {
    if d.IsZero() {
        return true // clearing is always allowed
    }
    return !d.Before(time.Unix(0, 0)) &&
        d.Before(time.Now().Add(10*365*24*time.Hour)) &&
        d.After(time.Now().Add(-30*24*time.Hour))
}

Type guard

func isDeadlineSanityErr(err error) bool {
    return errors.Is(err, sessionwatch.ErrDeadlineBeforeEpoch) ||
        errors.Is(err, sessionwatch.ErrDeadlineTooFarFuture) ||
        errors.Is(err, sessionwatch.ErrDeadlineInPast)
}

Try / catch

if err := w.Update(deadline); err != nil {
    if errors.Is(err, sessionwatch.ErrDeadlineBeforeEpoch) {
        // protocol glitch: watcher already cleared; force zero deadline into
        // other sinks and log the raw value for a server-side fix
    }
}

Prevention

When it happens

Trigger: Update(deadline) is called with a value where deadline.Before(time.Unix(0,0)) is true: management serialized a negative expiry, sent an unset field that deserializes to a negative value, or agent/management version skew misinterprets the field (for example a signed vs unsigned or unit mismatch producing negative nanoseconds).

Common situations: Management version that predates the session-expiry feature leaving the field unset in a way the agent misreads; protobuf schema drift between agent and management; a management bug setting expiry from an uninitialized variable.

Related errors


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