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:
- X-RateLimit-Reset-After header (seconds as float)
- 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