nats-io/nats-server · error

subject does not match jwt content

Error message

subject does not match jwt content

What it means

consumerMemStore.ForceUpdate performs the same sanity checks as Update but skips the normal state-validity comparison against current state; it still rejects a ConsumerState whose AckFloor.Consumer exceeds Delivered.Consumer. The invariant is fundamental, so even forced updates cannot bypass it.

Source

Thrown at server/accounts.go:4529

	for _, reqSub := range []string{accUpdateEventSubjOld, accUpdateEventSubjNew} {
		// subscribe to account jwt update requests
		if _, err := s.sysSubscribe(fmt.Sprintf(reqSub, "*"), func(_ *subscription, _ *client, _ *Account, subj, resp string, msg []byte) {
			var pubKey string
			tk := strings.Split(subj, tsep)
			if len(tk) == accUpdateTokensNew {
				pubKey = tk[accReqAccIndex]
			} else if len(tk) == accUpdateTokensOld {
				pubKey = tk[accUpdateAccIdxOld]
			} else {
				s.Debugf("DirResolver - jwt update skipped due to bad subject %q", subj)
				return
			}
			if claim, err := jwt.DecodeAccountClaims(string(msg)); err != nil {
				respondToUpdate(s, resp, "n/a", "jwt update resulted in error", err)
			} else if err := claimValidate(claim); err != nil {
				respondToUpdate(s, resp, claim.Subject, "jwt validation failed", err)
			} else if claim.Subject != pubKey {
				err := errors.New("subject does not match jwt content")
				respondToUpdate(s, resp, pubKey, "jwt update resulted in error", err)
			} else if claim.Issuer == op && strict {
				err := errors.New("operator requires issuer to be a signing key")
				respondToUpdate(s, resp, pubKey, "jwt update resulted in error", err)
			} else if err := dr.save(pubKey, string(msg)); err != nil {
				respondToUpdate(s, resp, pubKey, "jwt update resulted in error", err)
			} else {
				respondToUpdate(s, resp, pubKey, "jwt updated", nil)
			}
		}); err != nil {
			return fmt.Errorf("error setting up update handling: %v", err)
		}
	}
	if _, err := s.sysSubscribe(accClaimsReqSubj, func(_ *subscription, c *client, _ *Account, _, resp string, msg []byte) {
		// As this is a raw message, we need to extract payload and only decode claims from it,
		// in case request is sent with headers.
		_, msg = c.msgParts(msg)
		if claim, err := jwt.DecodeAccountClaims(string(msg)); err != nil {

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Sanitize the state (clamp consumer ack floor to delivered) before ForceUpdate
  2. Reject the incoming state at the sync layer instead of forcing it
  3. Re-fetch a consistent consumer state snapshot from the current leader

Example fix

// before
consumer.ForceUpdate(badState) // AckFloor.Consumer > Delivered.Consumer
// after
if badState.AckFloor.Consumer > badState.Delivered.Consumer {
    badState.AckFloor.Consumer = badState.Delivered.Consumer
}
consumer.ForceUpdate(badState)
Defensive patterns

Strategy: validation

Validate before calling

func validConsumerAckFloor(st *ConsumerState) bool {
    return st.AckFloor.Consumer <= st.Delivered.Consumer &&
        st.AckFloor.Stream <= st.Delivered.Stream
}
if !validConsumerAckFloor(st) { return ErrInvalidState }
consumer.ForceUpdate(st)

Try / catch

if err := consumer.ForceUpdate(st); err != nil {
    if strings.Contains(err.Error(), "bad ack floor for consumer") {
        st.AckFloor.Consumer = st.Delivered.Consumer
        err = consumer.ForceUpdate(st)
    }
}

Prevention

When it happens

Trigger: Calling consumerMemStore.ForceUpdate(state) where state.AckFloor.Consumer > state.Delivered.Consumer — typically during consumer state restore or leader takeover in a cluster.

Common situations: JetStream consumer state sync/restore from a peer with corrupt or reordered data; hand-built state in tests or migration tooling.

Related errors


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