Client
Async::Matrix::Client is how your service talks back to the homeserver. It wraps Async::HTTP::Internet — fiber-safe, with automatic connection pooling — and authenticates every request with the as_token from your config.
client = Async::Matrix::Client.new(config)
Convenience methods
The common actions have direct methods:
client.send_text(room_id, "Hello world")
client.send_html(room_id, "<b>bold</b>") # optional 3rd arg: plaintext fallback
client.send_notice(room_id, "Bot says hi")
client.join_room(room_id)
client.leave_room(room_id)
client.set_display_name("My Bot") # optional 2nd arg: user_id
client.whoami
For arbitrary message events there’s send_message_event(room_id, event_type, content).
The API chain
Anything beyond the convenience methods goes through client.api, a method-chained interface to the full Matrix Client-Server API. Each chained segment becomes a path component; the terminal .get/.post/.put/.delete dispatches the request. The path is validated at runtime against the official OpenAPI documents bundled in the gem, so a typo’d endpoint fails fast rather than hitting the homeserver:
client.api.createRoom.post(name: "Pub")
client.api.rooms("!room:ex.com").messages.get(dir: "b", limit: 10)
client.api.rooms(room_id).read_markers.post("m.fully_read" => event_id)
Path parameters are passed as call arguments (rooms("!room:ex.com")); query parameters and JSON bodies are passed to the terminal verb as keyword/hash arguments. Because the chain is built on BasicObject, even method-like segment names (send, display, format) route through the chain rather than colliding with Ruby’s own methods.
Binary routes — media upload, download, thumbnails — are detected from the path and dispatched through the MediaClient instead of the JSON client. Media endpoints that the spec pins to /v1 are rewritten automatically even if you chain them under /v3.
Low-level requests
If you need to bypass the chain entirely, the verb methods take a raw path:
client.get("/_matrix/client/v3/account/whoami")
client.put("/_matrix/client/v3/rooms/#{room_id}/send/m.room.message/#{txn}", body)
client.post(path, body, max_retries: 5)
Retries, backoff, and limits
The client hardens every request against a flaky or rate-limiting homeserver:
- Retryable statuses.
502,503,504are always retried;429is retried unless rate-limit handling is disabled. Up toDEFAULT_MAX_RETRIES(3) attempts; passmax_retries:per call to override (0 disables). - Gateway errors use exponential backoff with full jitter (
rand(0..delay), the AWS-recommended strategy) to avoid retry stampedes. - Rate limits honor the server’s
Retry-Afterheader — both delta-seconds (120) and HTTP-date forms — falling back to exponential backoff when it’s absent. - Response-size limiting. JSON responses are capped (50 MiB) and error bodies more tightly, enforced while streaming so an oversized body is cut off rather than buffered whole.
Media
client.media returns a MediaClient for binary transfers (also reachable via the API chain’s binary routes):
mxc = client.media.upload(:post, upload_path, bytes, "image/png")
data = client.media.download(download_path)
Uploads take an explicit content type (default application/octet-stream); downloads stream with the same size-limit enforcement as the JSON client. See Media for the full flow.
Lifecycle
Everything is fiber-safe and pools connections, so share one Client across all handlers — do not create one per event. Call client.close on shutdown to drain the pool.