async-matrixSourceAsyncMatrixMediaClient

class MediaClient

Async HTTP client for binary Matrix media operations.

Handles raw byte uploads and downloads against the Matrix content repository. Unlike the JSON-only Client, this never wraps request bodies in JSON.generate and can return raw binary response bodies.

Not intended to be used directly — the Chain dispatches here automatically when the request path matches a BINARY_ROUTES entry.

media_client = Async::Matrix::MediaClient.new(config) media_client.upload("POST", path, bytes, "image/png") bytes = media_client.download(path)

Definitions

UPLOAD_RESPONSE_SIZE_LIMIT = 1 * 1024 * 1024

Upload JSON responses are small; error bodies are even smaller.

def upload(method, path, body, content_type = "application/octet-stream")

Upload raw bytes to the content repository.

Implementation

def upload(method, path, body, content_type = "application/octet-stream")
  url = "#{@base}#{path}"
  headers = @auth_headers + [["content-type", content_type]]

  Console.debug(self) { "UPLOAD #{method} #{path} (#{content_type}, #{body.bytesize} bytes)" }

  response = internet.call(method, url, headers, body)
  status   = response.status

  unless (200..299).cover?(status)
    payload = read_limited(response, ERROR_RESPONSE_SIZE_LIMIT)
    parsed = ApplicationService::ErrorResponse.new(
      begin; JSON.parse(payload); rescue; {} end
    )
    Console.error(self) { "Matrix media upload #{status}: #{parsed.errcode} — #{parsed.error}" }
    raise HomeserverError.new(
      parsed.errcode || "UNKNOWN",
      parsed.error || payload.to_s[0..200],
      status: status
    )
  end

  payload = read_limited(response, UPLOAD_RESPONSE_SIZE_LIMIT)
  payload && !payload.empty? ? JSON.parse(payload) : {}
end

def download(path)

Download from the content repository.

Returns the raw HTTP response object so the caller has access to both the body and response metadata:

response = media_client.download(path) response.read # raw bytes (String, Encoding::BINARY) response.headers["content-type"] # "image/png" response.headers["content-disposition"] # "inline; filename="photo.jpg"" response.body.each |chunk| ... # streaming

Implementation

def download(path)
  url = "#{@base}#{path}"

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

  response = internet.call("GET", url, @auth_headers, nil)
  status   = response.status

  unless (200..299).cover?(status)
    payload = response.read
    parsed = ApplicationService::ErrorResponse.new(
      begin; JSON.parse(payload); rescue; {} end
    )
    Console.error(self) { "Matrix media download #{status}: #{parsed.errcode} — #{parsed.error}" }
    raise HomeserverError.new(
      parsed.errcode || "UNKNOWN",
      parsed.error || payload.to_s[0..200],
      status: status
    )
  end

  response
end

def read_limited(response, limit)

Read response body with a size limit. Raises ResponseTooLargeError if the body exceeds the limit.

Implementation

def read_limited(response, limit)
  body = response.body
  return nil unless body

  if body.respond_to?(:length) && body.length && body.length > limit
    body.close
    raise ResponseTooLargeError.new(
      "M_TOO_LARGE",
      "Response Content-Length #{body.length} bytes exceeds limit of #{limit} bytes"
    )
  end

  buffer = String.new(encoding: Encoding::BINARY)
  body.each do |chunk|
    buffer << chunk
    if buffer.bytesize > limit
      body.close
      raise ResponseTooLargeError.new(
        "M_TOO_LARGE",
        "Response body exceeds limit of #{limit} bytes"
      )
    end
  end
  buffer.empty? ? nil : buffer
end