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)
  • NoBody

    An 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.

Search Document