class THTP::Server

An HTTP (Rack middleware) implementation of Thrift-RPC

Attributes

service[R]

Public Class Methods

new(app = NullRoute.new, service:, handlers: []) click to toggle source

@param app [Object?] The Rack application underneath, if used as middleware @param service [Thrift::Service] The service class handled by this server @param handlers [Object,Array<Object>] The object(s) handling RPC requests

# File lib/thtp/server.rb, line 26
def initialize(app = NullRoute.new, service:, handlers: [])
  @app = app
  @service = service
  @handler = MiddlewareStack.new(service, handlers)
  @route = %r{^/#{canonical_name(service)}/(?<rpc>[\w.]+)/?$} # /:service/:rpc
end

Public Instance Methods

call(rack_env) click to toggle source

Rack implementation entrypoint

# File lib/thtp/server.rb, line 39
def call(rack_env)
  start_time = get_time
  # verify routing
  request = Rack::Request.new(rack_env)
  protocol = Encoding.protocol(request.media_type) || Thrift::JsonProtocol
  return @app.call(rack_env) unless request.post? && @route.match(request.path_info)
  # get RPC name from route
  rpc = Regexp.last_match[:rpc]
  raise UnknownRpcError, rpc unless @handler.respond_to?(rpc)
  # read, perform, write
  args = read_args(request.body, rpc, protocol)
  result = @handler.public_send(rpc, *args)
  write_reply(result, rpc, protocol).tap do
    publish :rpc_success,
            request: request, rpc: rpc, args: args, result: result, time: elapsed_ms(start_time)
  end
rescue Thrift::Exception => e # known schema-defined Thrift errors
  write_reply(e, rpc, protocol).tap do
    publish :rpc_exception,
            request: request, rpc: rpc, args: args, exception: e, time: elapsed_ms(start_time)
  end
rescue ServerError => e # known server/communication errors
  write_error(e, protocol).tap do
    publish :rpc_error,
            request: request, rpc: rpc, args: args, error: e, time: elapsed_ms(start_time)
  end
rescue => e # a non-Thrift exception occurred; translate to Thrift as best we can
  write_error(InternalError.new(e), protocol).tap do
    publish :internal_error, request: request, error: e, time: elapsed_ms(start_time)
  end
end
use(middleware_class, *middleware_args) click to toggle source

delegate to RPC handler stack

# File lib/thtp/server.rb, line 34
def use(middleware_class, *middleware_args)
  @handler.use(middleware_class, *middleware_args)
end

Private Instance Methods

read_args(request_body, rpc, protocol) click to toggle source

fetches args from a request

# File lib/thtp/server.rb, line 74
def read_args(request_body, rpc, protocol)
  args_struct = args_class(service, rpc).new
  # read off the request body into a Thrift args struct
  deserialize_stream(request_body, args_struct, protocol)
  # args are named methods, but handler signatures use positional arguments;
  # convert between the two using struct_fields, which is an ordered hash.
  args_struct.struct_fields.values.map { |f| args_struct.public_send(f[:name]) }
end
write_error(exception, protocol) click to toggle source

Given an unexpected error (non-schema), try to write it schemaless. The status code indicate to clients that an error occurred and should be deserialised. The implicit schema for a non-schema exception is:

struct exception { 1: string message, 2: i32 type }

@param exception [Errors::ServerError]

# File lib/thtp/server.rb, line 113
def write_error(exception, protocol)
  # write to the response as an EXCEPTION message
  [
    Status::EXCEPTION,
    { Rack::CONTENT_TYPE => Encoding.content_type(protocol) },
    [serialize_buffer(exception.to_thrift, protocol)],
  ]
end
write_reply(reply, rpc, protocol) click to toggle source

given any schema-defined response (success or exception), write it to the HTTP response

# File lib/thtp/server.rb, line 84
def write_reply(reply, rpc, protocol)
  result_struct = result_class(service, rpc).new
  # void return types have no spot in the result struct
  unless reply.nil?
    if reply.is_a?(Thrift::Exception)
      # detect the correct exception field, if it exists, and set its value
      field = result_struct.struct_fields.values.find do |f|
        f.key?(:class) && reply.instance_of?(f[:class])
      end
      raise BadResponseError, rpc, reply unless field
      result_struct.public_send("#{field[:name]}=", reply)
    else
      # if it's not an exception, it must be the "success" value
      result_struct.success = reply
    end
  end
  # write to the response as a REPLY message
  [
    Status::REPLY,
    { Rack::CONTENT_TYPE => Encoding.content_type(protocol) },
    [serialize_buffer(result_struct, protocol)],
  ]
end