Class: Twirp::Client
- Inherits:
-
Object
- Object
- Twirp::Client
- Extended by:
- ServiceDSL
- Defined in:
- lib/twirp/client.rb
Direct Known Subclasses
Class Method Summary collapse
-
.client_for(svclass) ⇒ Object
DSL (alternative) to define a client from a Service class.
- .error_from_response(resp) ⇒ Object
- .is_http_redirect?(status) ⇒ Boolean
- .make_http_request(conn, service_full_name, rpc_method, content_type, req_opts, body) ⇒ Object
-
.rpc_define_method(rpcdef) ⇒ Object
Hook for ServiceDSL#rpc to define a new method client.<ruby_method>(input, req_opts).
-
.twirp_error_from_intermediary(status, reason, body) ⇒ Object
Error that was caused by an intermediary proxy like a load balancer.
-
.twirp_redirect_error(status, location) ⇒ Object
Twirp clients should not follow redirects automatically, Twirp only handles POST requests, redirects should only happen on GET and HEAD requests.
Instance Method Summary collapse
-
#initialize(conn, opts = {}) ⇒ Client
constructor
Init with a Faraday connection, or a base_url that is used in a default connection.
-
#rpc(rpc_method, input, req_opts = nil) ⇒ Object
Make a remote procedure call to a defined rpc_method.
Methods included from ServiceDSL
package, package_name, rpcs, service, service_full_name, service_name
Constructor Details
#initialize(conn, opts = {}) ⇒ Client
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”.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# 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 |
Class Method Details
.client_for(svclass) ⇒ Object
DSL (alternative) to define a client from a Service class.
31 32 33 34 35 36 37 |
# 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) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# 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) ⇒ Boolean
108 109 110 |
# File 'lib/twirp/client.rb', line 108 def is_http_redirect?(status) status && status >= 300 && status <= 399 end |
.make_http_request(conn, service_full_name, rpc_method, content_type, req_opts, body) ⇒ Object
112 113 114 115 116 117 118 119 120 121 122 123 124 |
# 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 |
.rpc_define_method(rpcdef) ⇒ Object
Hook for ServiceDSL#rpc to define a new method client.<ruby_method>(input, req_opts).
40 41 42 43 44 45 46 |
# 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) ⇒ Object
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.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# 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) ⇒ Object
Twirp clients should not follow redirects automatically, Twirp only handles POST requests, redirects should only happen on GET and HEAD requests.
98 99 100 101 102 103 104 105 106 |
# 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 |
Instance Method Details
#rpc(rpc_method, input, req_opts = nil) ⇒ Object
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.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# 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(error: 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) rpc_response_thennable(resp) do |resp| rpc_response_to_clientresp(resp, content_type, rpcdef) end end |