nats-io/nats-server · error

operator %s expects version %s got error instead: %s

Error message

operator %s expects version %s got error instead: %s

What it means

Each operator claim carries AssertServerVersion; the server parses it during validation. If that version string is malformed, ParseServerVersion returns an error and the server wraps it here, refusing to start rather than guessing operator requirements.

Source

Thrown at server/jwt.go:130

				break
			}
		}
		if foundNonEmpty && !foundSys {
			return fmt.Errorf("system_account in config and operator JWT must be identical")
		}
	} else if o.TrustedOperators[0].SystemAccount == _EMPTY_ {
		// In case the system account is neither defined in config nor in the first operator.
		// If it would be needed due to the nats account resolver, raise an error.
		switch o.AccountResolver.(type) {
		case *DirAccResolver, *CacheDirAccResolver:
			return fmt.Errorf("using nats based account resolver - the system account needs to be specified in configuration or the operator jwt")
		}
	}

	srvMajor, srvMinor, srvUpdate, _ := versionComponents(VERSION)
	for _, opc := range o.TrustedOperators {
		if major, minor, update, err := jwt.ParseServerVersion(opc.AssertServerVersion); err != nil {
			return fmt.Errorf("operator %s expects version %s got error instead: %s",
				opc.Subject, opc.AssertServerVersion, err)
		} else if major > srvMajor {
			return fmt.Errorf("operator %s expected major version %d > server major version %d",
				opc.Subject, major, srvMajor)
		} else if srvMajor > major {
		} else if minor > srvMinor {
			return fmt.Errorf("operator %s expected minor version %d > server minor version %d",
				opc.Subject, minor, srvMinor)
		} else if srvMinor > minor {
		} else if update > srvUpdate {
			return fmt.Errorf("operator %s expected update version %d > server update version %d",
				opc.Subject, update, srvUpdate)
		}
	}
	// If we have operators, fill in the trusted keys.
	// FIXME(dlc) - We had TrustedKeys before TrustedOperators. The jwt.OperatorClaims
	// has a DidSign(). Use that longer term. For now we can expand in place.
	for _, opc := range o.TrustedOperators {

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Re-sign the operator with a valid assert version: `nsc edit operator --account-url ...` / reissue so AssertServerVersion like "2.10.0" is set
  2. Check the claim with `nsc describe operator` or decode the JWT and fix the assertion
  3. Use a current nsc/nats-server version when generating operator JWTs

Example fix

// before (decoded operator claim)
"nats": { "assertServerVersion": "" }
// after
"nats": { "assertServerVersion": "2.10.1" }
Defensive patterns

Strategy: validation

Validate before calling

// Go: verify operator assertion parses before loading config
if _, _, _, err := jwt.ParseServerVersion(opc.AssertServerVersion); err != nil {
    return fmt.Errorf("operator %s has invalid assert version", opc.Subject)
}

Prevention

When it happens

Trigger: An operator JWT whose AssertServerVersion is not a valid semantic version (empty, typo, non-numeric); validateOptions fails while iterating TrustedOperators.

Common situations: Hand-crafted or older operator JWTs with a missing/invalid `assertServerVersion` claim; corrupting the JWT during manual editing.

Related errors


AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02). Data as JSON: /api/errors/ec33fece19a27705. Report an issue: GitHub.