pub enum DispatchError {
Command(CommandError),
NoSuchRepo(RepoRef),
Denied {
principal: String,
repo: RepoRef,
needed: Access,
held: String,
},
Standby {
principal: String,
repo: RepoRef,
browse: bool,
},
Twin {
principal: String,
repo: RepoRef,
},
AnonymousElsewhere {
repo: RepoRef,
browse_host: Option<String>,
},
Plan {
namespace: String,
line: String,
},
Store(Error),
}Expand description
Why an exec request was not served.
Variants§
Command(CommandError)
The command did not parse. The bash case, and every other one.
NoSuchRepo(RepoRef)
The repository does not exist.
Denied
The principal does not hold enough access.
Fields
Standby
This box is a read-only STANDBY and the write is refused.
A distinct arm, not a DispatchError::Denied: the writer holds the
grant — on a primary this push would land — so “you do not have access”
would be a lie that sends them to an administrator about permissions.
The honest sentence is that this box does not take client writes; push
to the primary. It is only ever reached by a principal who already
passed the grant check, so it leaks no repository the writer could not
already see.
Fields
Twin
This box is a TWIN and serves no git at all — not a read, not a write, not to anybody (2026-09-21, lane T4).
Its own arm and not a DispatchError::Standby, because a standby is a
box that serves reads and refuses writes, and a twin is a box that
serves NEITHER: serve.rs says why in one line — a read would grow the
caches the twin must not hold — and the twin’s whole value is that its
disk holds the primary’s log and nothing derived.
Why this exists at the door rather than only at the bind. The bind
is chosen ONCE, in serve::start, from stores.is_twin() at that
instant. A box becomes a twin over the wire, through
Bootstrap.BecomeTwin, long after that — and the owner’s one-medium
ruling makes that the ONLY way it happens, because a twin is now
installed from the same image as a main with no per-box parameter to
say which it is. MEASURED under KVM by lane T3, 2026-09-21: a box
conferred at runtime kept 0.0.0.0:2222 bound for the rest of its life,
the this box is a TWIN warning never printed, and its volume ended up
holding five objects.exploded cache files. A socket that cannot be
moved without a restart is not a reason to serve git from it in the
meantime.
AnonymousElsewhere
Anonymous reads are not served here (--anonymous refuse); the
browse replica serves them, and browse_host names it when the
operator did. Raised before the repository is even looked up, so a
stranger learns nothing about what exists on the primary.
Fields
Plan
The gate chain refuses this: the disk is under its floor (every
build), or (metered) a Suspended or Retention tenant’s push or LFS
upload, an anonymous read of a lapsed tenant’s public repository, or a
push at the pack quota.
Its own arm for the same reason DispatchError::Standby is one: the
caller holds the grant, so “you do not have access” would be a lie
that sends a paying customer to an administrator about permissions.
The honest sentence names the fact — free bytes and the floor, or the
state, the date and where to renew — and it is only ever reached AFTER
the grant check admitted the request, so it leaks nothing about a
repository the caller could not already see.
Store(Error)
The identity table or the catalog could not be read.
Deliberately not folded into DispatchError::Denied. The tables
live in storage (the Arrow-IPC ref log) and a read can fail; answering a storage
fault with “you do not have access” would turn an outage into a silent,
convincing permission denial that nobody would think to investigate.
The ACL model is explicit that may(...) returns Result<bool> for this
reason, and this variant is where that Result lands.
Implementations§
Source§impl DispatchError
impl DispatchError
Sourcepub fn client_message(&self) -> Option<String>
pub fn client_message(&self) -> Option<String>
What the client is told.
None means “answer with a channel failure and say nothing”: the
request was not even a git command, so there is no git client on the
other end that would understand a pkt-line.
“No such repository” and “no permission” produce the same string on purpose. A client that could tell them apart could enumerate private repositories one guess at a time. The server log keeps the distinction; the wire does not.
Those two arms are one arm, and were two identical format!s until
2026-08-05. That is the twinning one implementation forbids, and this is the case it
forbids it for: the property is that the two strings are equal, so two
copies of the string put the property one careless edit away from being
false, in the direction where the wire starts leaking which private
repositories exist.
a_missing_repository_and_a_forbidden_one_are_one_string_to_the_client
guards it from the far side of a real session; this makes the guard hard
to need.
Trait Implementations§
Source§impl Debug for DispatchError
impl Debug for DispatchError
Source§impl Display for DispatchError
impl Display for DispatchError
Source§impl Error for DispatchError
impl Error for DispatchError
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()