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
- Re-sign the operator with a valid assert version: `nsc edit operator --account-url ...` / reissue so AssertServerVersion like "2.10.0" is set
- Check the claim with `nsc describe operator` or decode the JWT and fix the assertion
- 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
- Never hand-edit operator JWTs; reissue via nsc
- Decode and inspect operator claims with nsc before deployment
- Regenerate old operators lacking a valid assertServerVersion
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
- operators require an account resolver to be configured
- operators do not allow Accounts to be configured directly
- operators do not allow users to be configured directly
- conflicting options for 'TrustedKeys' and 'TrustedOperators'
- operator %s expected major version %d > server major version
AI-assisted analysis of nats-io/nats-server@3a66a489d2 (2026-09-02).
Data as JSON: /api/errors/ec33fece19a27705.
Report an issue: GitHub.