glyde/rest/headers
Reading Discord’s rate-limit headers, purely.
Deadlines come from the relative x-ratelimit-reset-after, never the
absolute x-ratelimit-reset, which is wrong by our clock’s drift.
Every counter is an Option: a header that did not arrive has to stay
distinguishable from one that arrived as zero.
Types
What the 429’s body added. A body in Discord’s rate-limit shape proves Discord answered, which rules out a block in front of it.
pub type BodyEvidence {
RateLimitBody(global: Bool)
NoBody
}
Constructors
-
RateLimitBody(global: Bool) -
NoBodyAn HTML block page, an empty body, or a caller holding only headers.
What one response taught us. The limiter’s own type, so outcome hands its
answer straight to limiter.Settled with no crossing.
pub type Outcome =
limiter.Outcome
Whose fault the 429 was, resolved. Only resolve_scope answers this, and
glyde/rest/error asks it through the same function, so the two cannot
disagree about a response. The limiter’s own type, so handing a reading
over costs nothing.
pub type Scope =
limiter.Scope
What x-ratelimit-scope said, before anything is made of it. Kept apart
from Scope so an unresolved reading cannot be passed off as the answer.
pub type ScopeHeader {
Named(limiter.Scope)
Unnamed(String)
Missing
}
Constructors
-
Named(limiter.Scope) -
Unnamed(String)A value Discord has never documented, kept as sent. It names no rule we know, so on its own it decides nothing.
-
Missing
Values
pub const fallback_retry_ms: Int
Our own number, for a 429 carrying neither the retry-after header nor the
body field.
pub fn header(
headers: List(#(String, String)),
name: String,
) -> option.Option(String)
One header by name, None when it did not arrive. Adapters hand names over
in any case, so both sides are lowercased for the match.
pub fn outcome(
status status: Int,
headers headers: List(#(String, String)),
body body: String,
) -> limiter.Outcome
Classify one response. Total: anything unparseable degrades to “learned
nothing”. body is read only on a 429; pass "" for the rest.
pub fn resolve_scope(
headers: List(#(String, String)),
body: BodyEvidence,
) -> limiter.Scope
Whose fault a 429 was, from everything the response says. The one
classifier: glyde/rest/error resolves the same 429 through this, so no
response can be a route limit to one reader and a global limit to the other.
pub fn scope_header(
headers: List(#(String, String)),
) -> ScopeHeader
What x-ratelimit-scope said, unresolved. resolve_scope is what turns it
into an answer.
pub fn seconds_to_ms(raw: String) -> Result(Int, Nil)
Seconds to milliseconds, rounding up, because waking early costs a 429.
Read as decimal text, so "0.1" is exactly 100 ms. A negative value is 0.