nats-io/nats-server · error

auth callout violation: %q on account %q

Error message

auth callout violation: %q on account %q

What it means

The auth callout (decentralized authentication) service on the account responded with an empty authorization response, i.e. a message containing nothing beyond the trailing CRLF. The server treats an empty response as a rejection with no reason and reports this violation naming 'no reason supplied' and the account.

Source

Thrown at server/auth_callout.go:99

		xkp, xkey = s.xkp, s.info.XKey
	}

	// Create a keypair for the user. We will expect this public user to be in the signed response.
	// This prevents replay attacks.
	ukp, _ := nkeys.CreateUser()
	pub, _ := ukp.PublicKey()

	reply := s.newRespInbox()
	respCh := make(chan string, 1)

	decodeResponse := func(rc *client, rmsg []byte, acc *Account) (*jwt.UserClaims, error) {
		account := acc.Name
		_, msg := rc.msgParts(rmsg)

		// This signals not authorized.
		// Since this is an account subscription will always have "\r\n".
		if len(msg) <= LEN_CR_LF {
			return nil, fmt.Errorf("auth callout violation: %q on account %q", "no reason supplied", account)
		}
		// Strip trailing CRLF.
		msg = msg[:len(msg)-LEN_CR_LF]
		encrypted := false
		// If we sent an encrypted request the response could be encrypted as well.
		// we are expecting the input to be `eyJ` if it is a JWT
		if xkp != nil && len(msg) > 0 && !bytes.HasPrefix(msg, []byte(jwtPrefix)) {
			var err error
			msg, err = xkp.Open(msg, pubAccXKey)
			if err != nil {
				return nil, fmt.Errorf("error decrypting auth callout response on account %q: %v", account, err)
			}
			encrypted = true
		}

		cr, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
		if err != nil {
			return nil, err

View on GitHub (pinned to 3a66a489d2)

Solutions

  1. Fix the auth callout service to publish a valid signed AuthorizationResponseClaims JWT in the response body.
  2. Log the raw response in the callout service to find where the empty body originates.
  3. Ensure the callout service only publishes responses to the reply subject and includes the error/ok decision field.
  4. If intentional rejection, respond with a properly signed response carrying an error reason instead of an empty message.

Example fix

// before (callout service)
nc.Publish(msg.Reply, []byte("\r\n"))
// after
resp, _ := jwt.EncodeAuthorizationResponseClaims(claims, signerKey)
nc.Publish(msg.Reply, []byte(resp))
Defensive patterns

Strategy: validation

Validate before calling

// in the auth callout service before publishing the response
if len(respBytes) <= 2 {
    return errors.New("refusing to publish empty authorization response")
}
nc.Publish(msg.Reply, respBytes)

Type guard

func hasBody(msg []byte) bool { return len(msg) > 2 }

Try / catch

user, err := s.lookupAccountForClientRequest(...)
if err != nil && strings.Contains(err.Error(), "auth callout violation") {
    log.Printf("auth callout returned empty response on %s", account)
    return nil, ErrAuthentication
}

Prevention

When it happens

Trigger: While processing an authorization request response (rc.msgParts on the reply message), if len(msg) <= LEN_CR_LF the library returns this error. Happens when the auth callout service publishes an empty message body to the access-control reply subject, or sends a response that is only CRLF.

Common situations: Auth callout service bug that replies without serializing the AuthorizationResponseClaims; service crashing and a stub/empty publish being sent; proxy or middleware stripping the payload; test harness replying with an empty ACK.

Related errors


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