Report a Refusal Off the Body

Table of Contents

+#+filetags: :pattern:nats:error-handling: +#+created: 2026-10-14 +#+updated: 2026-10-14 +#+pattern_group: error-handling

This page defines the nats pattern Report a Refusal Off the Body in the NATS service patterns language.

1. Also known as

  • NATS: a header reply; the X-Error envelope.

2. Context

A handler can fail before it has any domain answer to give: the caller has no credential, the credential expired, the caller lacks the permission, or the payload will not decode. None of these is a statement about the data the service owns.

3. Problem

How does a service tell a caller that its request was refused, when the reply envelope is a body the caller will try to decode?

4. Forces

  • A refusal has no meaningful body: no record was read and none was written.
  • A caller that decodes the reply as though it were a response reports "the payload did not match" for what is really an authorization failure.
  • Some refusals are actionable by the client (log in again) and some are not.
  • An answer the service decided is a different thing from a refusal, and the two must not be conflated.

5. Solution

Refuse with an empty body and a named code in the X-Error header:

error_reply(nats_, msg, ores::service::error_code::forbidden);
// publishes: reply subject, empty body, X-Error: forbidden

The codes are a closed set, and each has a meaning the caller can act on:

Code Meaning The caller should
unauthorized No credential, malformed credential, or bad signature Log in
token_expired A well-formed credential past its expiry Refresh the token, then retry once
forbidden Authenticated, but the permission is not granted Ask for the permission; do not retry
bad_request The payload did not decode Fix the payload; do not retry

Three rules:

  • A refusal carries no body. The header is the whole message. A caller that reads only the body must check the header first.
  • A refusal is not a decision. What the service decided — missing, conflict, denied by a business rule — is an answer, and travels in the body; see Report a Failure in the Body.
  • The two expiry codes get their own handling. They are not refusals to a caller that can renew; the client layer turns them into a refresh-and-retry or a session error, so the refusal helper deliberately ignores them.

6. Failure handling

Concern What the pattern does
Errors The refusal is a successful transport reply carrying a named code.
Timeouts A refusal arrives long before a deadline, so a caller never waits on one.
Retries Only token_expired is retried, and only once, after a forced refresh.
Idempotency A refused request was not handled, so re-sending it changes nothing.
Observability The handler logs the code and the subject; the reply itself carries no detail beyond the code.

7. Pitfalls

  • A refusal with a body. The caller decodes it and reports a schema error.
  • A refusal for a business decision. "The row is missing" is an outcome, not a refusal, and a caller cannot act on it as one.
  • A missing refusal. A handler that returns without replying leaves the caller to time out; see Never Let an Exception Cross the Bus.
  • A new code invented at a call site. The set is closed so callers can be exhaustive.

8. Security

This pattern is the refusal path, so it must not leak why beyond the code: no distinction between "no such account" and "wrong password", no permission names in the reply. See Identity and Access.

9. Resulting context

Benefits
A caller can tell a refusal from a payload error from a business outcome, and acts correctly on each.
Liabilities
Two reply shapes exist, so every caller must check the header before decoding.

10. In ORE Studio

`error_reply()` in `ores.service` publishes the empty body with the header, and `make_request_context` and `has_permission` produce the authorization codes. On the caller side, `throw_if_refused()` converts a refusal into a typed error so a decoder never sees one, while the HTTP routes inspect the header directly and map it to an HTTP status.

11. Related patterns

12. Sources

13. See also

  • NATS service patterns — the structure note that orders this pattern language, and where to read this page in it.

Emacs 29.3 (Org mode 9.6.15)