class Twirp::Service
Attributes
Whether to raise exceptions instead of handling them with exception_raised
hooks. Useful during tests to easily debug and catch unexpected exceptions.
Public Class Methods
Rack response with a Twirp::Error
# File lib/twirp/service.rb, line 32 def error_response(twerr) status = Twirp::ERROR_CODES_TO_HTTP_STATUS[twerr.code] headers = {'Content-Type' => Encoding::JSON} # Twirp errors are always JSON, even if the request was protobuf resp_body = Encoding.encode_json(twerr.to_h) [status, headers, [resp_body]] end
# File lib/twirp/service.rb, line 41 def initialize(handler) @handler = handler @before = [] @on_success = [] @on_error = [] @exception_raised = [] end
Public Instance Methods
Setup hook blocks.
# File lib/twirp/service.rb, line 51 def before(&block) @before << block; end
Rack app handler.
# File lib/twirp/service.rb, line 61 def call(rack_env) begin env = {} bad_route = route_request(rack_env, env) return error_response(bad_route, env) if bad_route @before.each do |hook| result = hook.call(rack_env, env) return error_response(result, env) if result.is_a? Twirp::Error end output = call_handler(env) return error_response(output, env) if output.is_a? Twirp::Error return success_response(output, env) rescue => e raise e if self.class.raise_exceptions begin @exception_raised.each{|hook| hook.call(e, env) } rescue => hook_e e = hook_e end twerr = Twirp::Error.internal_with(e) return error_response(twerr, env) end end
Call the handler method with input attributes or protobuf object. Returns a proto object (response) or a Twirp::Error
. Hooks are not called and exceptions are raised instead of being wrapped. This is useful for unit testing the handler. The env should include fake data that is used by the handler, replicating middleware and before hooks.
# File lib/twirp/service.rb, line 94 def call_rpc(rpc_method, input={}, env={}) base_env = self.class.rpcs[rpc_method.to_s] return Twirp::Error.bad_route("Invalid rpc method #{rpc_method.to_s.inspect}") unless base_env env = env.merge(base_env) input = env[:input_class].new(input) if input.is_a? Hash env[:input] = input env[:content_type] ||= Encoding::PROTO env[:http_response_headers] = {} call_handler(env) end
# File lib/twirp/service.rb, line 54 def exception_raised(&block) @exception_raised << block; end
# File lib/twirp/service.rb, line 58 def name; @name ||= self.class.service_name; end
# File lib/twirp/service.rb, line 53 def on_error(&block) @on_error << block; end
# File lib/twirp/service.rb, line 52 def on_success(&block) @on_success << block; end
Private Instance Methods
Call handler method and return a Protobuf Message or a Twirp::Error
.
# File lib/twirp/service.rb, line 159 def call_handler(env) m = env[:ruby_method] if !@handler.respond_to?(m) return Twirp::Error.unimplemented("Handler method #{m} is not implemented.") end out = @handler.send(m, env[:input], env) case out when env[:output_class], Twirp::Error out when Hash env[:output_class].new(out) else Twirp::Error.internal("Handler method #{m} expected to return one of #{env[:output_class].name}, Hash or Twirp::Error, but returned #{out.class.name}.") end end
# File lib/twirp/service.rb, line 190 def error_response(twerr, env) begin @on_error.each{|hook| hook.call(twerr, env) } self.class.error_response(twerr) rescue => e return exception_response(e, env) end end
# File lib/twirp/service.rb, line 199 def exception_response(e, env) raise e if self.class.raise_exceptions begin @exception_raised.each{|hook| hook.call(e, env) } rescue => hook_e e = hook_e end twerr = Twirp::Error.internal_with(e) self.class.error_response(twerr) end
# File lib/twirp/service.rb, line 153 def route_err(code, msg, req) Twirp::Error.new code, msg, twirp_invalid_route: "#{req.request_method} #{req.fullpath}" end
Parse request and fill env with rpc data. Returns a bad_route error if could not be properly routed to a Twirp
method. Returns a malformed error if could not decode the body (either bad JSON or bad Protobuf)
# File lib/twirp/service.rb, line 112 def route_request(rack_env, env) rack_request = Rack::Request.new(rack_env) if rack_request.request_method != "POST" return route_err(:bad_route, "HTTP request method must be POST", rack_request) end content_type = rack_request.get_header("CONTENT_TYPE") if !Encoding.valid_content_type?(content_type) return route_err(:bad_route, "Unexpected Content-Type: #{content_type.inspect}. Content-Type header must be one of #{Encoding.valid_content_types.inspect}", rack_request) end env[:content_type] = content_type path_parts = rack_request.fullpath.split("/") if path_parts.size < 3 || path_parts[-2] != self.full_name return route_err(:bad_route, "Invalid route. Expected format: POST {BaseURL}/#{self.full_name}/{Method}", rack_request) end method_name = path_parts[-1] base_env = self.class.rpcs[method_name] if !base_env return route_err(:bad_route, "Invalid rpc method #{method_name.inspect}", rack_request) end env.merge!(base_env) # :rpc_method, :input_class, :output_class input = nil begin input = Encoding.decode(rack_request.body.read, env[:input_class], content_type) rescue => e error_msg = "Invalid request body for rpc method #{method_name.inspect} with Content-Type=#{content_type}" if e.is_a?(Google::Protobuf::ParseError) error_msg += ": #{e.message.strip}" end return route_err(:malformed, error_msg, rack_request) end env[:input] = input env[:http_response_headers] = {} return end
# File lib/twirp/service.rb, line 176 def success_response(output, env) begin env[:output] = output @on_success.each{|hook| hook.call(env) } headers = env[:http_response_headers].merge('Content-Type' => env[:content_type]) resp_body = Encoding.encode(output, env[:output_class], env[:content_type]) [200, headers, [resp_body]] rescue => e return exception_response(e, env) end end