Skip to content

Twirp Clients

Mario Izquierdo edited this page Nov 24, 2020 · 8 revisions

Client Definition

Clients may be defined using the Twirp::Client DSL, which can be auto-generated from a .proto file (same as service definitions).

class HelloWorldClient < Twirp::Client
  package "example"
  service "HelloWorld"
  rpc :Hello, HelloRequest, HelloResponse, :ruby_method => :hello
end

Instantiate a Client

Clients can be instantiated with the service base url.

base_url = "http://localhost:3000/twirp"
client = HelloWorldClient.new(base_url)

For more advanced HTTP setup, use a Faraday connection. For example:

conn = Faraday.new(:url => 'http://localhost:3000/twirp') do |c|
  c.use Faraday::Request::Retry
  c.use Faraday::Request::BasicAuthentication, 'login', 'pass'
  c.use Faraday::Response::Logger # log to STDOUT
  c.use Faraday::Adapter::NetHttp # can use different HTTP libraries
end
client = HelloWorldClient.new(conn)

Faraday middleware is very powerful and can substantially modify HTTP requests and responses. Make sure not to use middleware that conflicts with the way Twirp clients encode and serialize requests. For example, do not use Faraday::Request::UrlEncoded, Faraday::Request::Multipart, FaradayMiddleware::ParseJson, Faraday::Response::RaiseError, etc.

Make RPC calls

Use the rpc method with the request object, or the ruby method. The following calls are equivalent for a HelloWorld client:

# Equivalent RPC calls
client.rpc(:Hello, HelloRequest.new(name: "World")) # generic rpc method
client.hello(HelloRequest.new(name: "World")) # DSL method with message object
client.hello(name: "World") # DSL method with message attributes

The response is a Twirp::ClientResponse object with the properties data (success) and error (failure).

resp = client.hello(name: "World")
if resp.error
  puts resp.error # Twirp::Error
else
  puts resp.data  # HelloResponse (response message class)
end

The Twirp::Error has a well defined set of error codes useful for inspection:

resp = client.hello(name: "World")
if resp.error
  case resp.error.code
  when :not_found
    puts "Not found: #{resp.error.msg}"
  when :permission_denied
    puts "Thou Shall No Pass!"
  else
    puts resp.error
  end
end

Service 4xx and 5xx errors are returned as resp.error values. But exceptions happening on the network level are raised as exceptions:

begin do
  resp = client.rpc(:Hello, name: "World")
  if resp.error
    puts "Service failed with status 4xx or 5xx"
  end
rescue StandardError => e
  puts "Something crashed on the Client, Faraday connection or HTTP adapter"
end

Protobuf or JSON

Twirp services can serialize RPC messages with either Protobuf or JSON, depending on the Content-Type header set by the client. Protobuf is used by default. To force-serialize with JSON, set the content_type option as 2nd argument when initializing the client:

client = Example::HelloWorldClient.new(conn, content_type: "application/json")
resp = client.hello(name: "World") # request and response serialized as JSON

Using Twirp::ClientJSON as a type-free alternative:

If you just want to make a few quick requests from the console and you don't have the proto file or code generator at hand, you can use a Twirp::ClientJSON. This doesn't require the service definition DSL, requests are free form key-values.

require 'twirp'

host = 'http://localhost:3000/twirp'
client = Twirp::ClientJSON.new(host, package: "example", service: "HelloWorld", strict: true)

resp = client.rpc(:Hello, name: "World") # serialized as application/json; strict=true
if resp.error
  puts resp.error # Twirp::Error
else
  puts resp.data # Hash with plain attributes form the JSON response (untyped response)
end