Errors and exit codes¶
Every failure names its kind, the object and both sides of a disagreement.
Every error also prints one hint: line on stderr telling you what to check next.
The taxonomy has sixteen variants and four exit codes, and the mapping between them lives in exactly one place: Error::exit_code in crates/nexus-raw-core/src/error.rs.
The taxonomy¶
The variant set mirrors the error enum, fields included.
Wrappers can construct and match them, because the fields are public.
The hint column quotes Error::hint() verbatim: the same string the CLI prints and JSON wrappers receive in the hint field.
| Variant | Meaning | Exit | Hint (verbatim) | Typical cause |
|---|---|---|---|---|
Mismatch { name, detail } |
digest or sibling disagreement on a completed artifact | 1 | the two sides diverge: delete or fix one copy, never let nxr overwrite a diverging object |
the same name was published twice with different content, or a sibling belongs to a foreign object |
Incomplete { names } |
the local bytes+marker+digest chain is not closed | 1 | rerun the same command: finished names are skipped and the rest is retried |
a previous run stopped mid-transfer, or a file was edited after its marker was written |
Missing { names } |
requested names exist neither locally nor remotely | 1 | the name is absent on both sides: check spelling and the enumeration source |
a typo, or a name list that drifted from what was published |
Enumerate { url, reason } |
down or ls has no enumeration source |
1 | repeat --name, use --names-from FILE, or use --ls when the server has the search API |
no --name, --names-from or --ls on the call |
UnsafeName { name, reason } |
a name failed the grammar | 2 | names must be relative paths of [A-Za-z0-9._-] segments: the .sha256 and .part suffixes are reserved |
spaces, unicode, empty segments or the reserved .sha256 and .part suffixes |
Misuse(String) |
bad flags, missing files, half-set credentials | 2 | check the command line arguments |
invocation mistakes the shell cannot catch |
Auth { url, reason } |
401 or 403, or credentials required but absent | 3 | pass -u user:pass or export NXR_AUTH (base64 user:pass) |
expired token, wrong password, anonymous write attempt |
ReadOnly { url, status } |
the repository refuses a deletion: 403/405 to DELETE | 1 | the repository answered {status} to DELETE: it is read-only or the credentials lack write access, so rerunning is safe and nothing was removed |
a read-only deployment, or credentials without write access |
Transport { url, detail } |
network, TLS, timeout or stall after retries | 3 | check the network: transfers are resumable, rerunning is safe |
server down, connection reset, stalled body |
Http { status: 404, url } |
the object or version does not exist | 1 | check the URL path and that the version or object exists |
a typo in the path, or a version never published |
Http { status, url } |
any other unexpected status | 3 | the server answered {status}: check the URL path and the server health |
a proxy answered 429, or the path hit a non-artifact route. A 429 and a 5xx are retryable, the 429 honoring Retry-After |
ServiceMissing { url, root } |
the service REST API answered 404: not a Nexus, or a version without the endpoint | 3 | the service API lives at the server root: try {root}/service/rest/v1/repositories |
service repos against a server without the management API |
SearchRepoMissing { url } |
the search API answered 400: the repository is missing on the server or is not a raw repository | 3 | the repository is missing on the server or is not a raw repository |
ls against a repository URL the server does not serve, or a name that exists only in another format |
Io { path, detail } |
a local filesystem failure | 1 | check the local filesystem: permissions, space, symlinks. Transfers are resumable, rerunning is safe |
a full disk, a missing directory, an unwritable part path |
SmallObjectCap { url, cap } |
a server answered a small-object GET with a body beyond the protocol bound | 3 | the server sent a small object beyond its protocol bound: check the server health, or a proxy answering for it |
a hostile or broken server, or a proxy answering for it |
ServerBody { url, detail } |
a server body the protocol requires to be JSON did not parse | 3 | the server answered with a body the protocol does not define: check the server health, or a proxy answering for it |
a broken server, or a proxy or login page answering for it |
RangeIgnored { url } |
a server answered a ranged GET with the whole body: Range is ignored |
3 | the server does not honor range requests: the download fell back to a single stream |
a server or proxy that strips Range, the segmented download degrades automatically |
Exit 1 is a data verdict, 2 a broken invocation, 3 a broken transport.
The same classes appear in the CLI exit code table and in Error::exit_code() for API wrappers.
Message anatomy¶
All failures go to stderr in one shape: an error: line, then a hint: line.
This refusal was produced by a real run: a local sibling that no longer matches its bytes stops the run before any byte moves.
$ nxr up dist-bad/ https://nexus.example.com/repository/raw-main/1.4.0/ --plan
error: mismatch: pinned.xml: local object is broken and must not be overwritten: digest mismatch: sibling 48f92f1ecbe87b5702a77327c4b1da6bb3f78218eb93edfcdb9c21624d430413, actual f7324dec778c7f17852694ef8c8624e9f1f55289010f77bf7f8b56e0d9088989
hint: the two sides diverge: delete or fix one copy, never let nxr overwrite a diverging object
$ echo $?
1
error:, the prefix every failure starts with on stderr.- the kind, one of
mismatch,missing,cannot enumerate,unsafe name,transport, … - the detail, such as names, digests, URLs or the server's own reason.
hint:, the next thing to check, one line.
Captured transcripts¶
Each transcript below was produced by a real nxr run against the deterministic failure mock (crates/mock-nexus), with hosts neutralized to nexus.example.com.
The messages and exit codes are verbatim.
Auth failure (exit 3)¶
Wrong credentials against a repository that requires auth.
The Auth variant's message already carries the remedy, and there are no retries.
$ nxr get https://nexus.example.com/repository/raw-main/1.4.0/app-1.4.0.zip -u ci-bot:wrong-pass -o app.zip
error: auth: https://nexus.example.com/repository/raw-main/1.4.0/app-1.4.0.zip: HTTP 401 Unauthorized; pass -u user:pass or export NXR_AUTH (base64 user:pass)
hint: pass -u user:pass or export NXR_AUTH (base64 user:pass)
$ echo $?
3
Missing credentials fail identically, because anonymous access earns the same 401.
Not found (exit 1)¶
A storage 404 is a data fact: the object or version is absent.
It becomes Http { status: 404 } with its own hint, and a retry cannot change it, so it must not share the transport class.
head is the exception: it reports any status as a result and exits 0.
$ nxr get https://nexus.example.com/repository/raw-main/1.4.0/no-such-object.bin -o x.bin
error: http 404: https://nexus.example.com/repository/raw-main/1.4.0/no-such-object.bin
hint: check the URL path and that the version or object exists
$ echo $?
1
Enumeration refusal (exit 1)¶
down refuses to guess what to fetch.
Nexus raw has no guaranteed directory listing, so a missing source is a data refusal instead of a network error.
$ nxr down https://nexus.example.com/repository/raw-main/1.4.1/ vendor/v141/
error: cannot enumerate: https://nexus.example.com/repository/raw-main/1.4.1/: no --name/--names-from/--ls given
hint: repeat --name, use --names-from FILE, or use --ls when the server has the search API
$ echo $?
1
Misuse (exit 2)¶
Half-set environment credentials are an invocation error, never a silent anonymous call.
$ env NXR_USERNAME=ci-bot nxr head https://nexus.example.com/repository/raw-main/1.4.0/app-1.4.0.zip
error: misuse: NXR_USERNAME and NXR_PASSWORD must be set together
hint: check the command line arguments
$ echo $?
2
The same class covers flag values:
$ nxr --workers 0 head https://nexus.example.com/repository/raw-main/1.4.0/app-1.4.0.zip
error: misuse: --workers must be in 1..=64, got 0
hint: check the command line arguments
$ echo $?
2
Missing name (exit 1)¶
A name that exists on neither side is collected and refused before any transfer.
$ nxr down https://nexus.example.com/repository/raw-main/1.4.0/ vendor/v3/ --name no-such-file.bin
error: missing: no-such-file.bin: exist nowhere
hint: the name is absent on both sides: check spelling and the enumeration source
$ echo $?
1
Unsafe name (exit 2)¶
The grammar rejects the file before anything is hashed or sent.
$ nxr up dist-broken/ https://nexus.example.com/repository/raw-main/broken/
error: unsafe name: bad name.zip: each segment must match [A-Za-z0-9._-]+, be 1..=255 bytes, and not be '.' or '..'
hint: names must be relative paths of [A-Za-z0-9._-] segments: the .sha256 and .part suffixes are reserved
$ echo $?
2
Transport (exit 3)¶
Two flavors: a connection that resets before answering, and a body that stops mid-flight.
$ nxr get https://nexus.example.com/reset/app.bin --retry 1
error: transport: https://nexus.example.com/reset/app.bin: error sending request for url (https://nexus.example.com/reset/app.bin)
hint: check the network: transfers are resumable, rerunning is safe
$ echo $?
3
$ nxr get https://nexus.example.com/slowdata/big.bin --stall-secs 1 --retry 1 -o app.bin
error: transport: https://nexus.example.com/slowdata/big.bin: stalled: no bytes for 1s
hint: check the network: transfers are resumable, rerunning is safe
$ echo $?
3
Foreign sibling (exit 1)¶
A remote sibling whose digest cannot belong to the bytes is never overwritten. The per-name failure is collected in the summary, then the run refuses.
$ nxr down https://nexus.example.com/repository/raw-main/foreign/ out/ --name app.bin
plan: 0 to upload, 1 to download, 0 up to date
uploaded 0, downloaded 0, skipped 0
failed: app.bin: downloaded digest bd4ef5a0775f7705a1758f3ecde220fd045cce6a3b3b816fc684aada794bc24a, remote sibling 0000000000000000000000000000000000000000000000000000000000000000
error: mismatch: app.bin: downloaded digest bd4ef5a0775f7705a1758f3ecde220fd045cce6a3b3b816fc684aada794bc24a, remote sibling 0000000000000000000000000000000000000000000000000000000000000000: remote content diverges from its sha-sibling
hint: the two sides diverge: delete or fix one copy, never let nxr overwrite a diverging object
$ echo $?
1
Local I/O (exit 1)¶
A local filesystem failure is data, not misuse: the command line may be perfect and the disk full.
$ nxr put https://nexus.example.com/repository/raw-main/foreign/app.bin -f extra.txt
error: io: extra.txt: No such file or directory (os error 2)
hint: check the local filesystem: permissions, space, symlinks. Transfers are resumable, rerunning is safe
$ echo $?
1
Incomplete at verify (exit 1)¶
verify is offline, so every data problem it finds is Incomplete.
The summary names what failed, then the error line repeats it.
$ nxr verify dist/1.4.0/
uploaded 0, downloaded 0, skipped 2
failed: pinned.xml
error: incomplete: pinned.xml
hint: rerun the same command: finished names are skipped and the rest is retried
$ echo $?
1
Exit-code matrix¶
The per-command tables live in the CLI page.
In short: 0 continue the pipeline, 1 stop and show stderr to a human, 2 fix the invocation and rerun, 3 retry later, because resume makes reruns cheap.
doctor reuses the same classes: a failed local check such as TLS verification off or settings out of range exits 2, a failed reachability probe exits 3, a 401 or 403 probe included.
Two outcomes look like failures and are not:
| Case | Exit | Why |
|---|---|---|
audit findings |
1 |
findings are data: the instrument worked when it found and reported them, the report is the payload |
doctor --deep warn |
0 |
a warn is advice: a stripped Range, an absent Content-Length and a dead search index each print a hint and still exit 0, exit 3 is for probes that physically failed |
Neither audit nor doctor --deep mutates the repository: both only read (see read-only diagnostics).
The refusal rule¶
Mismatch, Incomplete and Missing are refusals, not failed attempts.
nxr never overwrites a diverging completed artifact, never shadows a remote-only name with up, and never writes bytes that failed their digest check.
Recovery is a decision (fix or delete a copy, or republish under a new name), never a retry.
The classification that produces these verdicts is specified in the symmetric diff.
In NDJSON¶
Transfer-level failures surface in the summary's failed list, its structured failures list, and in retrying events rather than as separate error objects.
failed keeps the legacy strings, so a mismatch carries name: detail; failures carries {"code","name"} with the exit-code class.
{"attempt":2,"event":"retrying","name":"extra.txt","reason":"transport: https://nexus.example.com/repository/raw-main/flaky/app.bin: HTTP 503"}
{"downloaded":0,"event":"summary","kind":"transfer","failed":["app.bin: downloaded digest bd4ef5a0775f7705a1758f3ecde220fd045cce6a3b3b816fc684aada794bc24a, remote sibling 0000000000000000000000000000000000000000000000000000000000000000"],"failures":[{"code":1,"name":"app.bin"}],"skipped":0,"uploaded":0}
The error: and hint: lines still go to stderr, and the process exit code still carries the class.
In the Rust API¶
The taxonomy is nexus_raw_core::Error.
Error::exit_code() is the single mapping from variant to class, and Error::hint() returns the hint string.
Wrappers call these instead of re-deriving either.
match nxr.up(dir, None, true, None).await {
Ok(summary) => info!("published: {summary:?}"),
Err(e) if e.exit_code() == 3 => warn!("transport, retry later: {e}"),
Err(e) => error!("data or invocation problem, hint: {:?}", e.hint()),
}
The diff refusals are a separate type, nexus_raw_core::Verdict, which converts into Error with From, so a refused plan and a refused transfer look identical to a caller.
See the Rust API page for the full picture.
Where to go next¶
- Every flag and exit row per command: CLI reference.
- Live-incident recipes: when it breaks.
- Why the wire looks this way: the wire protocol.