Every failure fabric reports carries a stable code. A program reads the code;
the text beside it is for people and may change. Fabric 0.2.41 and later
carry codes. This page is the contract, and the test
fabric_config::error::tests::the_documented_table_is_the_code_list keeps the table below
identical to the code.
Each code has a class, and the class says what a caller should do:
refusal: trust or a grant said no. Retrying, or trying another route to the same machine, will not change that; only a change to trust will. Show it to a person as a refusal.fault: it did not work this time. Another attempt, later or by another route, may.usage: fabric rejected the arguments. Fix the call.
| Code | Class | Exit | Meaning |
|---|---|---|---|
peer_not_in_allow_list |
refusal | 77 | The other machine does not trust this NodeID, or stopped trusting it. |
peer_not_permitted_for_service |
refusal | 77 | The other machine trusts this NodeID but does not grant it this service, or removed the grant. |
unknown_peer |
fault | 69 | This machine has no trusted peer by that name or NodeID. |
peer_offline |
fault | 69 | The peer was recently found absent, so fabric did not try again yet. |
unreachable |
fault | 69 | Fabric tried and could not reach the peer. |
timeout |
fault | 69 | The caller's deadline passed before an answer. |
daemon_not_running |
fault | 69 | The local daemon is not running (fabric up starts it). |
daemon_stop_timeout |
fault | 69 | The daemon is still stopping after the bounded wait; its lease is not free yet. |
service_not_exposed |
fault | 69 | The peer is reachable but exposes no service by that name. |
service_unavailable |
fault | 69 | The service is exposed, but its socket or address could not be reached. |
session_limit_reached |
fault | 69 | The peer holds as many sessions as its limits allow, all attached. |
session_expired |
fault | 69 | A resume named a session the peer no longer holds. Start a new session; do not replay the old one. |
spawn_failed |
fault | 69 | The command behind an exec service or fabric exec could not be started. |
internal |
fault | 1 | Any failure not yet given a more specific code. |
runtime_peers_disabled |
refusal | 77 | The daemon was not started with --runtime-peers. |
peer_managed_statically |
refusal | 77 | This owner has no runtime record; static access is preserved. Inspect the effective-access sources. |
peer_owned_elsewhere |
refusal | 77 | This owner has no record for the node; another owner's access is preserved. |
policy_revision_conflict |
fault | 69 | The expected policy revision or list cursor is stale. Fetch a fresh snapshot and reconcile. |
runtime_state_unavailable |
fault | 69 | Runtime policy could not be read or committed. A faulted policy closes new network admissions; repair the file and explicitly reload. A narrow synchronous grant check can also return this transiently while a commit holds the policy barrier. |
runtime_commit_indeterminate |
fault | 69 | Rename succeeded but commit durability could not be confirmed. Admissions are quarantined. Resolve the durable request ID after repair and reload; do not invent a new request ID. |
runtime_receipt_capacity |
fault | 69 | The applicable retained receipt capacity is full: 960 ordinary slots, 64 more for withdrawal-only mutations. No-ops use no slot. Wait for retention expiry or reconcile through an existing receipt. |
pairing_exchange_inactive |
refusal | 77 | The attempt is not pending and cannot exchange messages. No delivery occurred; reconcile its status rather than assuming an empty success. |
request_id_conflict |
usage | 2 | A retained request ID was reused with different contents. |
cancellation_timeout |
fault | 69 | Policy committed, but selected work did not finish within two seconds. The receipt lists remaining admissions and sessions; policy is not rolled back. |
identity_mismatch |
usage | 2 | Active or prepared identity does not match the activation guards; nothing changed. |
identity_busy |
fault | 69 | An identity operation holds the lock (including through restart), or exposure changes are fenced by an identity handoff. Retry the same guarded activation or restart the daemon to recover an abandoned handoff. |
restart_failed |
fault | 69 | Identity changed, but the replacement daemon did not become ready. Retry the same guarded activation; never roll back the key. |
invalid_argument |
usage | 2 | Fabric rejected an argument's value. |
- Codes may be added in a minor release. None is removed or renamed, and no code moves to another class, before 2.0.
- A reader that meets a code it does not know acts on its class. The class is authoritative.
- A failure that gains a more specific code in a later release moves out of
internal, which is a fault either way.
A failing command exits with the status in the table: 77 for a refusal,
69 for a fault, 2 for a usage error (the status clap already uses for an
unknown flag or a missing argument) and 1 for internal, which is the status
every failure had before codes.
These commands keep the exit statuses they already documented, and report codes in their output instead:
fabric execexits with the remote command's status, so a status alone cannot tell a fabric failure from the command's own. Read the stderr line.fabric probeexits 0 supported, 1 unsupported, 2 unreachable, 3 timeout, and 64 when it cannot answer. These statuses are a legacy contract, kept for scripts that already read them; a grant refusal, for one, has always exited 2. The code is authoritative:probe --jsonprints its answer object, in whicherroris a string ornull, andcodeandclassname the code behind any answer other thansupported(bothnullforsupported). When it cannot answer,--jsonprints the error object instead. Seefabric probe --jsonbelow.fabric doctorexits 0 to 3 as its help describes.fabric update --checkexits 0 current, 1 available, 2 error.fabric dialdoes not connect: it resolves the peer and opens a local listener, so it fails only for what it can tell locally (unknown_peer,daemon_not_running,invalid_argument). A refusal or a fault met later surfaces as a closed local connection, with no text. To learn why, runfabric probe <peer> <service> --jsonright after and read itscode.fabric syncsubcommands other thanadd,rmandreloadare run by the sync companion, which is provisional and does not report codes yet. Their own--jsongoes after the subcommand:fabric sync ls --json.
The last line fabric writes to stderr for a failure is:
Error: fabric:<code> <text>
The text is for people. It keeps the words fabric printed before codes, so a script that looks for them still finds them.
With --json, a failing command also prints exactly one line on stdout, a JSON
object, and exits with the status above:
{"error":{"code":"peer_offline","class":"fault","message":"dial web on box: peer is offline","peer":"box","service":"web"}}codeandclassare as in the table.messageis the text for people.peeris the peer as the caller named it, andservicethe service, ornullwhen the failure concerns no single one. Both keys are always present.- An identity activation's
restart_failedadditionally carriespreviousandactiveNodeIDs. The rename has happened; retry the same guarded activation to reconcile the daemon, not to replace or roll back the key again. - Identity control requests and native service commands have finite timeouts,
reported as
timeout(orrestart_failedafter the rename). A pre-rename failure after shutdown triggers a best-effort restart of the current identity; its error says whether that restart succeeded. The key remains prepared. - A pre-rename abort removes
identity.exposuresand clears the exposure fence when the old daemon is reachable. If the CLI crashes after the snapshot or cannot reach that daemon, repeat the same guarded activation or restart it:expose/unexposeotherwise continue returningidentity_busy. - Nothing else is printed on stdout for a failure. Stderr still carries the human line; do not parse it when you have the JSON.
Every probe prints one line. When the probe was answered, it is the answer object, which has the same keys every time:
{"peer":"server","peer_id":"<node id>","protocol":"web","outcome":"supported","round_trip_micros":812,"transport":"direct","error":null,"code":null,"class":null}| Case | Exit | outcome |
code |
class |
|---|---|---|---|---|
| The service answered | 0 | supported |
null |
null |
| The peer exposes no such service | 1 | unsupported |
service_not_exposed |
fault |
| The peer does not grant the service to this machine | 2 | unreachable |
peer_not_permitted_for_service |
refusal |
| The peer does not trust this machine | 2 | unreachable |
peer_not_in_allow_list |
refusal |
| No answer, or no path to the peer | 2 | unreachable |
unreachable or peer_offline |
fault |
The --timeout passed |
3 | timeout |
timeout |
fault |
error is the far side's text, or null. When the probe could not be asked
at all (no daemon, an unknown peer name, an invalid --timeout), it exits 64
and prints the error object instead, for example
{"error":{"code":"unknown_peer","class":"fault",...,"peer":"ghost","service":null}}.
A build before 0.2.41 prints the same answer object without code and
class. When it cannot answer, it prints nothing on stdout and exits 64.
A clap usage error (an unknown flag, a missing argument) is found before
fabric knows --json was asked for. It exits 2 with usage text on stderr and
prints nothing on stdout.
A build older than 0.2.41 does not know --json on most commands, and answers
the same way: exit 2, usage text on stderr, nothing on stdout. A reader can
treat "no JSON object on stdout" as an older build and fall back to its text.
fabric probe --json and fabric sync ls --json existed before codes. On
probe, an error object is one whose error is an object (a probe answer's
error is a string or null); or compare fabric --version.
A machine-to-machine refusal or failure carries the same token, followed by one ASCII space and the text fabric sent before codes:
fabric:<code> <text>
<code> is [a-z][a-z0-9_]*. It appears in:
- The reason of an application close with code 403, on the direct exposure
wire (docs/tunnel-wire.md) and between daemons. For
example
fabric:peer_not_in_allow_list node is not in fabric allow-listandfabric:peer_not_permitted_for_service peer not permitted for service "web". The close code stays 403 for both refusals, so a client that reads only the code is unaffected. - The text of an Error frame (kind 5) on the direct exposure wire, for example
fabric:session_expired .... - The denial a daemon sends another daemon for a stream it will not serve, so a command run on one machine reports the code the other machine chose.
A reader finds the token by its fabric: prefix followed by a code and a
space; the old text after it is unchanged, so a reader that matched the old
text with a substring search keeps working. A reader that compared the whole
reason for equality must look for the text after the token instead.
A daemon older than 0.2.41 sends the old text without a token. Fabric reads
not permitted for service as peer_not_permitted_for_service and
node is not in fabric allow-list as peer_not_in_allow_list, and other
clients can do the same.