pub enum JitError {
Forbidden {
target: String,
runner_group_id: u64,
message: Option<String>,
},
NotFound {
target: String,
runner_group_id: u64,
message: Option<String>,
},
Rejected {
target: String,
message: Option<String>,
},
RateLimited(RateLimited),
Cancelled,
Github(GithubError),
}Expand description
Everything a just-in-time registration can fail with.
The three GitHub answers c4’s specification names are distinct outcomes,
not one error, because an operator’s next action differs for each and a
caller’s does too: 403 is terminal and needs a permissions or runner-group
change, 404 means the target or the group is not there, and 422 means the
request itself was rejected.
§None of these carries the encoded configuration
Every variant is built from the request — target, runner group, name — and
from GitHub’s own message field. A failing response has no
encoded_jit_config to leak, and a 201 that fails to decode is reported
through GithubError::Decode, which carries a serde_json::Error and not
the body. an_error_never_carries_the_encoded_configuration pins it.
Variants§
Forbidden
GitHub refused: the permission or the runner group does not allow it.
Terminal. Nothing retries this, and nothing may: d17 is the record
of what a 403 on this family of endpoints means and what it does not.
NotFound
GitHub found neither the target nor the runner group.
Separate from JitError::Forbidden because v1 measured both answers
from the same mistake: a group that does not exist is 404, a group that
exists but is not administrable is 403. Collapsing them tells an
operator to create a group that is already there.
Rejected
GitHub rejected the request body: the name or the label set.
RateLimited(RateLimited)
GitHub is rate limiting this credential. Resolves by waiting, and is the one failure here that is not about the request.
Cancelled
The caller withdrew the registration before it completed.
Github(GithubError)
Implementations§
Source§impl JitError
impl JitError
Sourcepub fn is_terminal(&self) -> bool
pub fn is_terminal(&self) -> bool
Whether retrying this exact request could ever produce a different answer.
403, 404 and 422 are all true here, and a 403 must be:
c4’s specification says “a 403 must never become a retry loop”, and
d17 is the record of a design that spent a spike discovering what a
403 on this family of endpoints means. A rejected credential is
terminal too — it resolves by an interactive auth login, not by
retrying.
§Why an undecodable answer is terminal, and why it is the expensive one
A body this client cannot parse will not parse on the next attempt, so
the answer to the question in the first line is plainly no. What makes
it worth spelling out is the cost of getting it wrong here rather than
anywhere else: generate-jitconfig answers 201 by completing a
registration, and the decode happens after that. A caller that read
this as retryable would issue a second registration, and a third, each
one a real runner created at GitHub whose one-shot configuration this
process then discards — a target silently accumulating registered
runners that never come online. So GithubError::Decode and
GithubError::Malformed are terminal, and
JitError::operator_action answers for both rather than leaving the
failure silent.
§Why GithubError::Forbidden and a 404 under Github are not
Both are reachable through the transparent #[from] without passing
RestJit::classify, and both would be terminal if they had. They stay
as they are because answering them here means keeping a second
status-code table beside classify’s, and the two would drift. For the
403 it is worse than untidy: GitHub answers a secondary rate limit
with a 403, classify runs RateLimited::detect first for
exactly that reason, and a predicate that called the raw variant
terminal without repeating that detection would turn the one 403 that
resolves by waiting into a permanent failure. An unclassified failure
reported as retryable costs a wasted request; an unclassified rate limit
reported as terminal costs the registration. The fix for those two is to
route them through classify, which every path inside this crate
already does.
Sourcepub fn rate_limited(&self) -> Option<&RateLimited>
pub fn rate_limited(&self) -> Option<&RateLimited>
The rate limit behind this failure, when there is one.
pub fn is_cancelled(&self) -> bool
Sourcepub fn operator_action(&self) -> Option<String>
pub fn operator_action(&self) -> Option<String>
What an operator can actually do about this, or None when there is
nothing for them to do.
Every terminal outcome has one, which is what “terminal and
operator-actionable” means: a failure a human cannot act on and a
program will not retry is a dead end. None is correct for a rate limit
and a cancellation — both resolve without anyone doing anything.
Trait Implementations§
Source§impl Error for JitError
impl Error for JitError
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()