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-Errorenvelope.
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
- Report a Failure in the Body — the other, opposite report.
- Classify Transport Failure — how a caller tells this from a timeout.
12. Sources
- NATS entity protocol specification — the error model on the wire.
- NATS documentation, Headers — the header mechanism the refusal uses.
13. See also
- NATS service patterns — the structure note that orders this pattern language, and where to read this page in it.