async-matrixSourceAsyncDiscordClient

class Client

Async HTTP client for the Discord REST API.

Authenticated with a Bot token. All methods are fiber-safe and run naturally inside Falcon's async reactor.

client = Async::Discord::Client.new(token: "MTk...") client.api.channels("123").messages.post(content: "hello") client.api.users("@me").get

Definitions

DEFAULT_MAX_RETRIES = 3

Retry defaults

RATE_LIMIT_STATUS = 429

Status codes eligible for retry

DEFAULT_RESPONSE_SIZE_LIMIT = 50 * 1024 * 1024

Response size limits (bytes)

def api

Returns a Gateway that provides method-chained access to every Discord HTTP API endpoint. Chains are validated against the official OpenAPI path tree and terminated by .get(), .post(), .put(), .patch(), or .delete().

client.api.channels("123").messages.post(content: "hello") client.api.guilds("789").get client.api.users("@me").get

Implementation

def api
  Api::Gateway.new(self)
end

def request(method, path, body = nil, max_retries: nil)

General-purpose request method supporting any HTTP method.

Implementation

def request(method, path, body = nil, max_retries: nil)
  url = "#{@base}#{path}"
  json_body = body ? JSON.generate(body) : nil
  effective_max_retries = max_retries || @max_retries

  Console.debug(self) { "#{method} #{path}" }

  attempt = 0
  loop do
    response = internet.call(method, url, @headers, json_body)
    status   = response.status

    if (200..299).cover?(status)
      payload = read_limited(response, @response_size_limit)
      return payload && !payload.empty? ? JSON.parse(payload) : {}
    end

    attempt += 1

    if attempt <= effective_max_retries && retryable_status?(status)
      delay = compute_retry_delay(status, response, attempt)
      Console.warn(self) {
        "#{method} #{path} returned #{status}, retry #{attempt}/#{effective_max_retries} in #{delay.round(2)}s"
      }
      response.close if response.respond_to?(:close)
      sleep(delay)
      next
    end

    payload = read_limited(response, @error_response_size_limit)
    parsed = begin; JSON.parse(payload || "{}"); rescue; {} end
    discord_code = parsed["code"]
    discord_msg  = parsed["message"] || payload.to_s[0..200]

    Console.error(self) { "Discord API #{status}: #{discord_code} — #{discord_msg}" }

    error_class = case status
                  when 401 then AuthError
                  when 429 then RateLimitError
                  when 400..499 then ApiError
                  else ServerError
                  end

    raise error_class.new(
      discord_code.to_s,
      discord_msg,
      status: status
    )
  end
end

def compute_retry_delay(status, response, attempt)

Discord sends Retry-After as seconds (float) in the JSON body on 429, and also as X-RateLimit-Reset-After header. We check both.

Implementation

def compute_retry_delay(status, response, attempt)
  if status == RATE_LIMIT_STATUS
    server_delay = parse_rate_limit_delay(response)
    delay = server_delay || exponential_delay(attempt)
    [delay, @max_retry_delay].min
  else
    calculated = exponential_delay(attempt)
    rand(0.0..[calculated, @max_retry_delay].min)
  end
end

def parse_rate_limit_delay(response)

Parse Discord rate limit delay. Checks:

  1. X-RateLimit-Reset-After header (seconds as float)
  2. Retry-After header (seconds as integer)

Implementation

def parse_rate_limit_delay(response)
  reset_after = response.headers["x-ratelimit-reset-after"]
  return reset_after.to_f if reset_after

  retry_after = response.headers["retry-after"]
  return retry_after.to_f if retry_after && retry_after.strip.match?(/\A[\d.]+\z/)

  nil
end