Class: RightDevelop::Testing::Server::MightApi::App::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/right_develop/testing/servers/might_api/app/base.rb

Direct Known Subclasses

Echo, Playback, Record

Defined Under Namespace

Classes: MightError, MissingRoute

Constant Summary collapse

MAX_REDIRECTS =

500 after so many redirects

10

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(state_file_name) ⇒ Base

Returns a new instance of Base.



41
42
43
44
45
46
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 41

def initialize(state_file_name)
  @config = ::RightDevelop::Testing::Server::MightApi::Config
  @logger = ::RightDevelop::Testing::Server::MightApi.logger

  @state_file_path = state_file_name ? ::File.join(@config.fixtures_dir, state_file_name) : nil
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



39
40
41
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 39

def config
  @config
end

#loggerObject (readonly)

Returns the value of attribute logger.



39
40
41
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 39

def logger
  @logger
end

#state_file_pathObject (readonly)

Returns the value of attribute state_file_path.



39
40
41
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 39

def state_file_path
  @state_file_path
end

Instance Method Details

#call(env) ⇒ 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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 48

def call(env)
  env['rack.logger'] ||= logger

  # read body from stream.
  request = ::Rack::Request.new(env)
  body = request.body.read

  # proxy any headers from env starting with HTTP_
  headers = env.inject({}) do |r, (k,v)|
    # note that HTTP_HOST refers to this proxy server instead of the
    # proxied target server. in the case of AWS authentication, it is
    # necessary to pass the value through unmodified or else AWS auth
    # fails.
    if k.start_with?('HTTP_')
      r[k[5..-1]] = v
    end
    r
  end

  # special cases.
  ['ACCEPT', 'CONTENT_TYPE', 'CONTENT_LENGTH', 'USER_AGENT'].each do |key|
    headers[key] = env[key] unless env[key].to_s.empty?
  end

  # handler
  verb = request.request_method
  uri = ::URI.parse(request.url)
  handle_request(env, verb, uri, headers, body)
rescue MissingRoute => e
  message = "#{e.class} #{e.message}"
  logger.error(message)
  if config.routes.empty?
    logger.error("No routes configured.")
  else
    logger.error("The following routes are configured:")
    config.routes.keys.each do |prefix|
      logger.error("  #{prefix}...")
    end
  end

  # not a 404 because this is a proxy/stub service and 40x might appear to
  # have come from a proxied request/response whereas 500 is never an
  # expected response.
  internal_server_error(message)
rescue ::RightDevelop::Testing::Client::Rest::Request::Playback::PlaybackError => e
  # response has not been recorded.
  message = e.message
  trace = [e.class.name] + (e.backtrace || [])
  logger.error(message)
  logger.debug(trace.join("\n"))
  internal_server_error(message)
rescue ::Exception => e
  message = "Unhandled exception: #{e.class} #{e.message}"
  trace = e.backtrace || []
  if logger
    logger.error(message)
    logger.debug(trace.join("\n"))
  else
    env['rack.errors'].puts(message)
    env['rack.errors'].puts(trace.join("\n"))
  end
  internal_server_error(message)
end

#find_route(uri) ⇒ Array

Returns pair of [prefix, data] or nil.

Parameters:

  • uri (URI)

    path to find

Returns:

  • (Array)

    pair of [prefix, data] or nil



236
237
238
239
240
241
242
243
244
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 236

def find_route(uri)
  find_path = uri.path
  logger.debug "Route URI path to match = #{find_path.inspect}"
  config.routes.find do |prefix, data|
    matched = find_path.start_with?(prefix)
    logger.debug "Tried = #{prefix.inspect}, matched = #{matched}"
    matched
  end
end

#handle_request(env, verb, uri, headers, body) ⇒ TrueClass

Handler.

Parameters:

  • env (Hash)

    from rack

  • verb (String)

    as one of [‘GET’, ‘POST’, etc.]

  • uri (URI)

    parsed from full url

  • headers (Hash)

    for proxy call with any non-proxy data omitted

  • body (String)

    streamed from payload or empty

Returns:

  • (TrueClass)

    always true

Raises:

  • (::NotImplementedError)


121
122
123
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 121

def handle_request(env, verb, uri, headers, body)
  raise ::NotImplementedError, 'Must be overridden'
end

#internal_server_error(message) ⇒ Array

Returns rack-style response for 500.

Returns:

  • (Array)

    rack-style response for 500



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 313

def internal_server_error(message)
  formal = "MightAPI internal error\n\nProblem:\n  \#{message}\n"

  [
    500,
    {
      'Content-Type'   => 'text/plain',
      'Content-Length' => ::Rack::Utils.bytesize(formal).to_s
    },
    [formal]
  ]
end

#normalize_rack_response_headers(headers) ⇒ Hash

rack has a convention of newline-delimited header multi-values.

HACK: changes underscore to dash to defeat RestClient::AbstractResponse line 27 (on client side) from failing to parse cookies array; it incorrectly calls .inject on the stringized form instead of using the raw array form or parsing the cookies into a hash, but only if the raw name is ‘set_cookie’ (‘set-cookie’ is okay).

even wierder, on line 78 it assumes the raw name is ‘set-cookie’ and that works out for us here.

Parameters:

  • headers (Hash)

    to normalize

Returns:

  • (Hash)

    normalized headers



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 295

def normalize_rack_response_headers(headers)
  result = headers.inject({}) do |h, (k, v)|
    h[k.to_s.gsub('_', '-').downcase] = v.join("\n")
    h
  end

  # a proxy server must always instruct the client close the connection by
  # specification because a live socket cannot be proxied from client to
  # the real server. this also works around a lame warning in ruby 1.9.3
  # webbrick code (fixed in 2.1.0+) saying:
  #   Could not determine content-length of response body.
  #   Set content-length of the response or set Response#chunked = true
  # in the case of 204 empty response, which is incorrect.
  result['connection'] = 'close'
  result
end

#proxy(request_class, verb, uri, headers, body, throttle = nil) ⇒ Array

Makes a proxied API request using the given request class.

Parameters:

  • request_class (Class)

    for API call

  • verb (String)

    as one of [‘GET’, ‘POST’, etc.]

  • uri (URI)

    parsed from full url

  • headers (Hash)

    for proxy call with any non-proxy data omitted

  • body (String)

    streamed from payload or empty

  • throttle (Integer) (defaults to: nil)

    for playback or nil

Returns:

  • (Array)

    rack-style tuple of [code, headers, [body]]



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 135

def proxy(request_class, verb, uri, headers, body, throttle = nil)

  # check routes.
  unless route = find_route(uri)
    raise MissingRoute, "No route configured for #{uri.path.inspect}"
  end
  route_path, route_data = route
  response = nil
  max_redirects = MAX_REDIRECTS
  while response.nil? do
    request_proxy = nil
    begin
      proxied_url = ::File.join(route_data[:url], uri.path)
      unless uri.query.to_s.empty?
        proxied_url << '?' << uri.query
      end
      proxied_headers = proxy_headers(headers, route_data)

      request_options = {
        fixtures_dir:    config.fixtures_dir,
        logger:          logger,
        route_path:      route_path,
        route_data:      route_data,
        state_file_path: state_file_path,
        method:          verb.downcase.to_sym,
        url:             proxied_url,
        headers:         proxied_headers,
        payload:         body
      }
      request_options[:throttle] = throttle if throttle
      request_proxy = request_class.new(request_options)

      # log normalized data for obfuscation.
      logger.debug("normalized request headers = #{request_proxy.request_metadata.headers.inspect}")
      logger.debug("normalized request body:\n" << request_proxy..body)

      request_proxy.execute do |rest_response, rest_request, net_http_response, &block|

        # headers.
        response_headers = normalize_rack_response_headers(net_http_response.to_hash)

        # eliminate headers that interfere with response via proxy.
        %w(
          status content-encoding
        ).each { |key| response_headers.delete(key) }

        case response_code = Integer(rest_response.code)
        when 301, 302, 307
          raise RestClient::Exceptions::EXCEPTIONS_MAP[response_code].new(rest_response, response_code)
        else
          # special handling for chunked body.
          if response_headers['transfer-encoding'] == 'chunked'
            response_body = ::Rack::Chunked::Body.new([rest_response.body])
          else
            response_body = [rest_response.body]
          end
          response = [response_code, response_headers, response_body]
        end
      end

      # log normalized data for obfuscation.
      logger.debug("normalized response headers = #{request_proxy.response_metadata.headers.inspect}")
      logger.debug("normalized response body:\n" << request_proxy..body.to_s)
    rescue RestClient::RequestTimeout
      net_http_response = request_proxy.handle_timeout
      response_code = Integer(net_http_response.code)
      response_headers = normalize_rack_response_headers(net_http_response.to_hash)
      response_body = [net_http_response.body]
      response = [response_code, response_headers, response_body]
    rescue RestClient::Exception => e
      case e.http_code
      when 301, 302, 307
        max_redirects -= 1
        raise MightError.new('Exceeded max redirects') if max_redirects < 0
        if location = e.response.headers[:location]
          redirect_uri = ::URI.parse(location)
          redirect_uri.path = ''
          redirect_uri.query = nil
          logger.debug("#{e.message} from #{route_data[:url]} to #{redirect_uri}")
          route_data[:url] = redirect_uri.to_s

          # move to end of FIFO queue for retry.
          request_proxy.forget_outstanding_request
        else
          logger.debug("#{e.message} was missing expected location header.")
          raise
        end
      else
        raise
      end
    ensure
      # remove from FIFO queue in case of any unhandled error.
      request_proxy.forget_outstanding_request if request_proxy
    end
  end
  response
end

#proxy_headers(headers, route_data) ⇒ Mash

Sets the header style using configuration of the proxied service.

Parameters:

  • headers (Hash)

    for proxy

  • route_data (Hash)

    containing header configuration, if any

Returns:

  • (Mash)

    proxied headers



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/right_develop/testing/servers/might_api/app/base.rb', line 252

def proxy_headers(headers, route_data)
  proxied = nil
  if proxy_data = route_data[:proxy]
    if header_data = proxy_data[:header]
      to_separator = (header_data[:separator] == :underscore) ? '_' : '-'
      from_separator = (to_separator == '-') ? '_' : '-'
      proxied = headers.inject(::Mash.new) do |h, (k, v)|
        k = k.to_s
        case header_data[:case]
        when nil
          k = k.gsub(from_separator, to_separator)
        when :lower
          k = k.downcase.gsub(from_separator, to_separator)
        when :upper
          k = k.upcase.gsub(from_separator, to_separator)
        when :capitalize
          k = k.split(/-|_/).map { |word| word.capitalize }.join(to_separator)
        else
          raise ::ArgumentError,
                "Unexpected header case: #{route_data.inspect}"
        end
        h[k] = v
        h
      end
    end
  end
  proxied || ::Mash.new(headers)
end