Judgment of an HTTP response. Zero dependencies. Says which of a closed set of things a server answered, and how long to wait.
use handan::{judge, Verdict};
// GitHub's secondary rate limit: a 403 that is really a throttle.
let j = judge(403, Some("60"));
assert_eq!(j.verdict(), Verdict::Throttled);
assert!(j.is_transient());
// A genuine authorization failure — the same status code.
assert_eq!(judge(403, None).verdict(), Verdict::Unauthorized);No async runtime, no HTTP client, no serde. It takes a u16 and a &str, so
one judgment serves reqwest, ureq, hyper, or a status recovered from
another tool's output.
Six crates in the pleme-io fleet independently derived this same shape, none aware of the others — verified: no shared ancestor, no cross-reference, no shared dependency.
| site | its derivation |
|---|---|
todoku |
parse_retry_after; retry_statuses: {429, 500, 502, 503, 504} |
| a content-sync crate | StatusCode newtype; RateLimited { retry_after_secs } |
sui-spec |
HttpError::Throttled { retry_after } + UnexpectedStatus |
| a GitOps client | RateLimited { retry_after_secs } |
forge |
is_transient |
fleet |
string-matched "HTTP error 429" out of another tool's English prose |
Two of the six live in closed repos and are described rather than named; the derivations are quoted exactly either way.
Duplication says a shape is convenient. Six independent derivations — two of them landing on the identical constant — says the shape is forced by the problem. That is the stronger signal, and the reason this is extracted rather than left alone.
Two of the six disagreed, and merging them forced a decision instead of preserving both:
501is not retryable.todokuretried{429, 500, 502, 503, 504}; that content-sync crate retried every5xx. The explicit set wins —501 Not Implementedand505are permanent refusals, so retrying them is waste that also consumes a retry budget a transient failure needed.- A third derivation settled the
4xxmembers.forgeholdsTRANSIENT_HTTP_STATUS_CODES = ["500","502","503","504","408","421","429"]— the same core set again, reached separately a third time. Reading it corrected this crate twice: it already had408 Request Timeout(which an earlier draft of this file claimed as an addition "neither original covered"), and it has421 Misdirected Request, which handan was missing.421belongs — RFC 9110 §15.5.20 says a client may retry it over a different connection. Its comment states the complement identically: "the terminal 4xx family (400/401/403/404) is absent by construction: retrying cannot help, so failing fast preserves the budget." Retry-Afterhas two forms and nobody parsed the second. A503or429carrying the HTTP-date form read fleet-wide as no advice at all, so every consumer discarded the one number the server volunteered and guessed instead. handan parses both, with no dependency: IMF-fixdate is a fixed ASCII grammar, so integer arithmetic suffices.
The header participates in the verdict. A 403 means opposite things with
and without Retry-After — GitHub sends 403 + Retry-After for a secondary
rate limit (wait it out) and bare 403 for a real denial (fix credentials). A
status-only classifier must round one into the other, and both roundings send
the operator the wrong way. Callers holding the signal but no parseable value
use [judge_signalled] rather than fabricating a header.
It classifies the status, never the situation. A 404 does not prove
absence on a private resource: GitHub answers 404 instead of 401 there on
purpose, so a 401 cannot disclose the resource exists. Verdict::Absent is
the safe generic reading, and a consumer that knows the resource is private is
right to override it.
[dependencies]
handan = "0.1"MIT.