glyde/rest/error

What Discord says when it says no. Split by body shape, not status code: the five bodies behind a non-2xx share almost no fields.

Only what one HTTP response carries. The caller does the IO, so a dead socket is theirs to describe.

Types

pub type ApiError {
  Discord(
    status: Int,
    code: ErrorCode,
    message: String,
    fields: List(FieldError),
    raw: String,
  )
  RateLimited(status: Int, limit: RateLimit)
  OAuth(
    status: Int,
    error: OAuthError,
    description: option.Option(String),
  )
  Opaque(
    status: Int,
    content_type: option.Option(String),
    body: String,
  )
  NotText(
    status: Int,
    content_type: option.Option(String),
    bytes: Int,
  )
  Malformed(DecodeFailure)
}

Constructors

  • Discord(
      status: Int,
      code: ErrorCode,
      message: String,
      fields: List(FieldError),
      raw: String,
    )

    A 4xx or 5xx carrying a Discord error object. fields is empty unless Discord sent a nested errors object, which in practice means 50035.

  • RateLimited(status: Int, limit: RateLimit)

    A 429 from Discord itself. Its body has no code and no errors.

  • OAuth(
      status: Int,
      error: OAuthError,
      description: option.Option(String),
    )

    The OAuth2 token endpoints answer in RFC 6749’s shape, which shares not one field with Discord’s own.

  • Opaque(
      status: Int,
      content_type: option.Option(String),
      body: String,
    )

    A non-2xx whose body was none of the above: a Cloudflare ban page, an HTML 502, an empty body. A 429 lands here when its body is not Discord’s rate-limit shape, because that is a ban and retrying extends it.

  • NotText(
      status: Int,
      content_type: option.Option(String),
      bytes: Int,
    )

    The bytes were not UTF-8, so there is no text to read or parse at all. Discord answers in UTF-8 everywhere, so this came from something in between. bytes is the length, because the body cannot be shown.

  • Malformed(DecodeFailure)

    A 2xx whose payload did not match the decoder.

What a code says about the request that produced it. is_transient and is_token_fatal are questions asked of this, so a code cannot be filed one way and answered another.

pub type CodeClass {
  TokenFatal
  PathTokenDead
  Forbidden
  Gone
  ResourceRateLimited
  Transient
  BadRequest
  Moderation
  UnknownClass
}

Constructors

  • TokenFatal

    The client’s own token is finished: stop, do not retry.

  • PathTokenDead

    The webhook or interaction token in the URL is gone. Says nothing about the client’s token, which is the whole point of keeping it apart.

  • Forbidden

    Permanently wrong for this caller or this input.

  • Gone

    The thing addressed is not there any more. Drop it from any cache.

  • ResourceRateLimited

    Rate-limit shaped, but on a resource rather than a route bucket. Do not feed these to the limiter.

  • Transient

    Discord’s own wording tells you to try again.

  • BadRequest

    The request has to change before it can work.

  • Moderation

    The content has to change, not the call.

  • UnknownClass

    A code this build does not name, and general_error, which names no class either. Discord adds codes continuously.

A decode failure that kept the raw text, which the stdlib error drops. Without it, a shape change and a wrong decoder look the same afterwards.

pub type DecodeFailure {
  DecodeFailure(error: json.DecodeError, raw: String)
}

Constructors

Discord’s code field. Open, not an enum: Discord calls its own table neither exhaustive nor stable, so a closed set would reject new codes.

pub opaque type ErrorCode

One leaf of the recursive errors object. code is an open set of strings Discord has never documented, empty when the leaf was a bare string.

pub type FieldError {
  FieldError(
    path: List(PathSegment),
    code: String,
    message: String,
  )
}

Constructors

  • FieldError(
      path: List(PathSegment),
      code: String,
      message: String,
    )

The error an OAuth2 token endpoint answers with, from RFC 6749 section 5.2. Open at the tail, like every other enum we read off the wire.

pub type OAuthError {
  InvalidRequest
  InvalidClient
  InvalidGrant
  UnauthorizedClient
  UnsupportedGrantType
  InvalidScope
  UnknownOAuthError(String)
}

Constructors

  • InvalidRequest

    The request is missing a parameter, or repeats one.

  • InvalidClient

    The client id or secret is wrong. Re-authenticating with them will not work either.

  • InvalidGrant

    The code, refresh token or grant is expired, revoked, or was issued to another client.

  • UnauthorizedClient

    This client is not allowed this grant type.

  • UnsupportedGrantType

    The grant type is one the endpoint does not support.

  • InvalidScope

    A scope was unknown, malformed, or wider than the one already granted.

  • UnknownOAuthError(String)

    A name RFC 6749 does not define, kept as sent.

One step of the path into the request body Discord rejected. Index stays apart from Key so the array versus object distinction survives.

pub type PathSegment {
  Key(String)
  Index(Int)
}

Constructors

  • Key(String)
  • Index(Int)
pub type RateLimit {
  RateLimit(
    retry_after: Float,
    scope: limiter.Scope,
    code: option.Option(ErrorCode),
    bucket: option.Option(String),
  )
}

Constructors

  • RateLimit(
      retry_after: Float,
      scope: limiter.Scope,
      code: option.Option(ErrorCode),
      bucket: option.Option(String),
    )

    Arguments

    retry_after

    Seconds, from the body. The Retry-After header is the same number rounded up to a whole second.

    scope

    The answer headers.resolve_scope gives, from the scope header, the body’s global flag and the shape of the response. is_global asks the only question most callers have.

    code

    Present when the limit is on a resource, slowmode say, not a route.

    bucket

    Absent on a global 429, which carries no bucket headers at all.

pub type Retry {
  DoNotRetry
  RetryAfter(seconds: Float)
  RetryWithBackoff
}

Constructors

  • DoNotRetry
  • RetryAfter(seconds: Float)

    Wait this many seconds. The caller sleeps, because glyde has no clock.

  • RetryWithBackoff

Values

pub const announcement_rate_limit: ErrorCode
pub const api_resource_overloaded: ErrorCode
pub const automod_blocked: ErrorCode
pub const automod_title_blocked: ErrorCode
pub const cannot_act_on_dm: ErrorCode
pub const cannot_message_user: ErrorCode
pub const cannot_send_sticker: ErrorCode
pub const channel_write_rate_limit: ErrorCode
pub fn classify(code: ErrorCode) -> CodeClass

Every code this build names, and what it means for the caller. The numbers are the constants above, spelled out because Gleam has no constants in patterns and this is asked several times per failed request.

pub const cloudflare_blocked: ErrorCode

A well formed Discord error, unlike the Cloudflare ban page, which has no JSON body at all and lands in Opaque.

pub fn code_from_int(value: Int) -> ErrorCode
pub fn code_to_int(code: ErrorCode) -> Int
pub fn counts_as_invalid_request(error: ApiError) -> Bool

Whether this counts against Discord’s budget of 10,000 invalid requests per 10 minutes, which ends in a Cloudflare ban of the whole IP.

pub fn describe(error: ApiError) -> String

One line per failure, for a host that just wants to print it. A caller acting on a 429 reads retry_advice off the typed value instead.

pub const empty_message: ErrorCode
pub const entity_too_large: ErrorCode
pub const explicit_content_blocked: ErrorCode
pub const feature_temporarily_disabled: ErrorCode
pub const file_too_large: ErrorCode
pub fn from_response(
  status status: Int,
  headers sent: List(#(String, String)),
  body body: String,
) -> ApiError

Classify a non-2xx by the shape of its body.

pub const general_error: ErrorCode

Discord’s catch-all: it names no class, which is why classify answers UnknownClass for it. On a 401 that means this call was rejected, where any other code on a 401 means the token is finished. See is_token_fatal.

pub const guild_write_rate_limit: ErrorCode
pub const harmful_link_blocked: ErrorCode
pub const index_not_available: ErrorCode
pub const interaction_already_acknowledged: ErrorCode

You already responded. Send a follow-up instead.

pub const interaction_send_failed: ErrorCode
pub const invalid_api_version: ErrorCode

glyde built a URL for an API version Discord no longer serves. Our bug.

pub const invalid_authentication_token: ErrorCode
pub const invalid_client_secret: ErrorCode
pub const invalid_file: ErrorCode
pub const invalid_form_body: ErrorCode

The one that carries a nested errors object.

pub const invalid_json: ErrorCode

glyde sent JSON Discord could not parse. Our bug.

pub const invalid_oauth2_token: ErrorCode
pub const invalid_webhook_token: ErrorCode
pub fn is_global(limit: RateLimit) -> Bool

Whether the whole token is limited rather than one route.

pub fn is_path_token_dead(error: ApiError) -> Bool

The webhook or interaction token in the URL is gone: deleted, rotated, or spent. Fix it by getting a new one, not by re-authenticating.

pub fn is_token_fatal(error: ApiError) -> Bool

The client’s own token is finished and it must stop, not retry. A 401 carrying any code other than 0 is Discord rejecting the token, not the call, so an unnamed code on a 401 still counts.

pub const max_old_message_edits: ErrorCode
pub const messages_temporarily_disabled: ErrorCode
pub const missing_access: ErrorCode
pub const missing_oauth2_scope: ErrorCode
pub const missing_permissions: ErrorCode
pub const no_mutual_guilds: ErrorCode
pub const not_authorized_for_application: ErrorCode
pub fn not_text(
  status status: Int,
  headers sent: List(#(String, String)),
  bytes bytes: Int,
) -> ApiError

For a response whose body would not become a String. Separate from from_response, which classifies by what the text says.

pub fn oauth_error_to_string(error: OAuthError) -> String

The name as the endpoint spells it, which is what a log should show.

pub const owner_only: ErrorCode
pub fn path_to_string(path: List(PathSegment)) -> String

Render a path as embed.fields[0].value. The empty path is the request as a whole, which Discord reports by putting _errors at the root.

pub const reaction_blocked: ErrorCode
pub const resource_rate_limited: ErrorCode
pub fn retry_advice(error: ApiError) -> Retry
pub const slowmode_rate_limit: ErrorCode
pub const too_few_or_many_to_delete: ErrorCode
pub const too_many_followups: ErrorCode
pub const too_old_to_bulk_delete: ErrorCode
pub const two_factor_required: ErrorCode
pub const unauthorized: ErrorCode
pub const unique_username_failed: ErrorCode
pub const unknown_ban: ErrorCode
pub const unknown_channel: ErrorCode
pub const unknown_emoji: ErrorCode
pub const unknown_guild: ErrorCode
pub const unknown_interaction: ErrorCode

The three second deadline for the first response passed, or the token was already spent. Retrying always fails.

pub const unknown_member: ErrorCode
pub const unknown_message: ErrorCode
pub const unknown_role: ErrorCode
pub const unknown_token: ErrorCode
pub const unknown_user: ErrorCode
pub const unknown_webhook: ErrorCode

Also what a follow-up gets once the 15 minute interaction token expires.

pub const verification_required: ErrorCode
Search Document