class Twirp::Client

Public Class Methods

client_for(svclass) click to toggle source

DSL (alternative) to define a client from a Service class.

# File lib/twirp/client.rb, line 31
def client_for(svclass)
  package svclass.package_name
  service svclass.service_name
  svclass.rpcs.each do |rpc_method, rpcdef|
    rpc rpc_method, rpcdef[:input_class], rpcdef[:output_class], ruby_method: rpcdef[:ruby_method]
  end
end
error_from_response(resp) click to toggle source
# File lib/twirp/client.rb, line 48
def error_from_response(resp)
  status = resp.status

  if is_http_redirect? status
    return twirp_redirect_error(status, resp.headers['Location'])
  end

  err_attrs = nil
  begin
    err_attrs = Encoding.decode_json(resp.body)
  rescue JSON::ParserError
    return twirp_error_from_intermediary(status, "Response is not JSON", resp.body)
  end

  code = err_attrs["code"]
  if code.to_s.empty?
    return twirp_error_from_intermediary(status, "Response is JSON but it has no \"code\" attribute", resp.body)
  end
  code = code.to_s.to_sym
  if !Twirp::Error.valid_code?(code)
    return Twirp::Error.internal("Invalid Twirp error code: #{code}", invalid_code: code.to_s, body: resp.body)
  end

  Twirp::Error.new(code, err_attrs["msg"], err_attrs["meta"])
end
is_http_redirect?(status) click to toggle source
# File lib/twirp/client.rb, line 108
def is_http_redirect?(status)
  status >= 300 && status <= 399
end
make_http_request(conn, service_full_name, rpc_method, content_type, req_opts, body) click to toggle source
# File lib/twirp/client.rb, line 112
def make_http_request(conn, service_full_name, rpc_method, content_type, req_opts, body)
  conn.post do |r|
    r.url "#{service_full_name}/#{rpc_method}"
    r.headers['Content-Type'] = content_type
    r.body = body

    if req_opts && req_opts[:headers]
      req_opts[:headers].each do |k, v|
        r.headers[k] = v
      end
    end
  end
end
new(conn, opts={}) click to toggle source

Init with a Faraday connection, or a base_url that is used in a default connection. Clients use Content-Type=“application/protobuf” by default. For JSON clinets use :content_type => “application/json”.

# File lib/twirp/client.rb, line 132
def initialize(conn, opts={})
  @conn = case conn
    when String then Faraday.new(url: conn) # init with hostname
    when Faraday::Connection then conn      # init with connection
    else raise ArgumentError.new("Invalid conn #{conn.inspect}. Expected String hostname or Faraday::Connection")
  end

  @content_type = (opts[:content_type] || Encoding::PROTO)
  if !Encoding.valid_content_type?(@content_type)
    raise ArgumentError.new("Invalid content_type #{@content_type.inspect}. Expected one of #{Encoding.valid_content_types.inspect}")
  end

  @service_full_name = self.class.service_full_name # defined through DSL
end
rpc_define_method(rpcdef) click to toggle source

Hook for ServiceDSL#rpc to define a new method client.<ruby_method>(input, req_opts).

# File lib/twirp/client.rb, line 40
def rpc_define_method(rpcdef)
  unless method_defined? rpcdef[:ruby_method] # collision with existing rpc method
    define_method rpcdef[:ruby_method] do |input, req_opts=nil|
      rpc(rpcdef[:rpc_method], input, req_opts)
    end
  end
end
twirp_error_from_intermediary(status, reason, body) click to toggle source

Error that was caused by an intermediary proxy like a load balancer. The HTTP errors code from non-twirp sources is mapped to equivalent twirp errors. The mapping is similar to gRPC: github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md. Returned twirp Errors have some additional metadata for inspection.

# File lib/twirp/client.rb, line 78
def twirp_error_from_intermediary(status, reason, body)
  code = case status
    when 400 then :internal
    when 401 then :unauthenticated
    when 403 then :permission_denied
    when 404 then :bad_route
    when 429, 502, 503, 504 then :unavailable
    else :unknown
  end

  Twirp::Error.new(code, code.to_s, {
    http_error_from_intermediary: "true",
    not_a_twirp_error_because: reason,
    status_code: status.to_s,
    body: body.to_s,
  })
end
twirp_redirect_error(status, location) click to toggle source

Twirp clients should not follow redirects automatically, Twirp only handles POST requests, redirects should only happen on GET and HEAD requests.

# File lib/twirp/client.rb, line 98
def twirp_redirect_error(status, location)
  msg = "Unexpected HTTP Redirect from location=#{location}"
  Twirp::Error.new(:internal, msg, {
    http_error_from_intermediary: "true",
    not_a_twirp_error_because: "Redirects not allowed on Twirp requests",
    status_code: status.to_s,
    location: location.to_s,
  })
end

Public Instance Methods

rpc(rpc_method, input, req_opts=nil) click to toggle source

Make a remote procedure call to a defined rpc_method. The input can be a Proto message instance, or the attributes (Hash) to instantiate it. Returns a ClientResp instance with an instance of output_class, or a Twirp::Error. The input and output classes are the ones configued with the rpc DSL. If rpc_method was not defined with the rpc DSL then a response with a bad_route error is returned instead.

# File lib/twirp/client.rb, line 151
def rpc(rpc_method, input, req_opts=nil)
  rpcdef = self.class.rpcs[rpc_method.to_s]
  if !rpcdef
    return ClientResp.new(nil, Twirp::Error.bad_route("rpc not defined on this client"))
  end

  content_type = (req_opts && req_opts[:headers] && req_opts[:headers]['Content-Type']) || @content_type

  input = rpcdef[:input_class].new(input) if input.is_a? Hash
  body = Encoding.encode(input, rpcdef[:input_class], content_type)

  resp = self.class.make_http_request(@conn, @service_full_name, rpc_method, content_type, req_opts, body)
  if resp.status != 200
    return ClientResp.new(nil, self.class.error_from_response(resp))
  end

  if resp.headers['Content-Type'] != content_type
    return ClientResp.new(nil, Twirp::Error.internal("Expected response Content-Type #{content_type.inspect} but found #{resp.headers['Content-Type'].inspect}"))
  end

  data = Encoding.decode(resp.body, rpcdef[:output_class], content_type)
  return ClientResp.new(data, nil)
end