class Twirp::Service

Attributes

raise_exceptions[RW]

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

error_response(twerr) click to toggle source

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
new(handler) click to toggle source
# File lib/twirp/service.rb, line 41
def initialize(handler)
  @handler = handler

  @before = []
  @on_success = []
  @on_error = []
  @exception_raised = []
end

Public Instance Methods

before(&block) click to toggle source

Setup hook blocks.

# File lib/twirp/service.rb, line 51
def before(&block) @before << block; end
call(rack_env) click to toggle source

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_rpc(rpc_method, input={}, env={}) click to toggle source

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
exception_raised(&block) click to toggle source
# File lib/twirp/service.rb, line 54
def exception_raised(&block) @exception_raised << block; end
full_name() click to toggle source

Service full_name is needed to route http requests to this service.

# File lib/twirp/service.rb, line 57
def full_name; @full_name ||= self.class.service_full_name; end
name() click to toggle source
# File lib/twirp/service.rb, line 58
def name; @name ||= self.class.service_name; end
on_error(&block) click to toggle source
# File lib/twirp/service.rb, line 53
def on_error(&block) @on_error << block; end
on_success(&block) click to toggle source
# File lib/twirp/service.rb, line 52
def on_success(&block) @on_success << block; end

Private Instance Methods

call_handler(env) click to toggle source

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
error_response(twerr, env) click to toggle source
# 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
exception_response(e, env) click to toggle source
# 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
route_err(code, msg, req) click to toggle source
# 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
route_request(rack_env, env) click to toggle source

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
success_response(output, env) click to toggle source
# 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