Class: Tilia::Http::Client

Inherits:
Event::EventEmitter
  • Object
show all
Defined in:
lib/tilia/http/client.rb

Overview

A rudimentary HTTP client.

This object wraps PHP’s curl extension and provides an easy way to send it a Request object, and return a Response object.

This is by no means intended as the next best HTTP client, but it does the job and provides a simple integration with the rest of sabre/http.

This client emits the following events:

before_request(RequestInterface request)
after_request(RequestInterface request, ResponseInterface response)
error(RequestInterface request, ResponseInterface response, bool &retry, int retry_count)
exception(RequestInterface request, ClientException e, bool &retry, int retry_count)

The beforeRequest event allows you to do some last minute changes to the request before it’s done, such as adding authentication headers.

The afterRequest event will be emitted after the request is completed succesfully.

If a HTTP error is returned (status code higher than 399) the error event is triggered. It’s possible using this event to retry the request, by setting retry to true.

The amount of times a request has retried is passed as retry_count, which can be used to avoid retrying indefinitely. The first time the event is called, this will be 0.

It’s also possible to intercept specific http errors, by subscribing to for example ‘error:401’.

Constant Summary collapse

STATUS_SUCCESS =
0
STATUS_CURLERROR =
1
STATUS_HTTPERROR =
2

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializevoid

Initializes the client.



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/tilia/http/client.rb', line 37

def initialize
  super

  @hydra = nil
  @throw_exceptions = false
  @max_redirects = 5
  @curl_settings = {
    header: false, # RUBY otherwise header will be part of response.body
    nobody: false,
    useragent: "tilia-http/#{Version::VERSION} (http://sabre.io/)"
  }
  @client_map = {}
end

Instance Attribute Details

#throw_exceptions=(value) ⇒ Boolean (writeonly)

If this is set to true, the Client will automatically throw exceptions upon HTTP errors.

This means that if a response came back with a status code greater than or equal to 400, we will throw a ClientHttpException.

This only works for the send method. Throwing exceptions for send_async is not supported.

Returns:

  • (Boolean)


235
236
237
# File 'lib/tilia/http/client.rb', line 235

def throw_exceptions=(value)
  @throw_exceptions = value
end

Instance Method Details

#add_curl_setting(name, value) ⇒ void

This method returns an undefined value.

Adds a CURL setting.

These settings will be included in every HTTP request.

Parameters:

  • name (Symbol)
  • value


244
245
246
# File 'lib/tilia/http/client.rb', line 244

def add_curl_setting(name, value)
  @curl_settings[name] = value
end

#create_client(request) ⇒ Object

TODO: document



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/tilia/http/client.rb', line 361

def create_client(request)
  settings = {}
  @curl_settings.each do |key, value|
    settings[key] = value
  end

  case request.method
  when 'HEAD'
    settings[:nobody] = true
    settings[:method] = :head
    settings[:postfields] = ''
    settings[:put] = false
  when 'GET'
    settings[:method] = :get
    settings[:postfields] = ''
    settings[:put] = false
  else
    settings[:method] = request.method.downcase.to_sym
    body = request.body
    if !body.is_a?(String) && !body.nil?
      settings[:put] = true
      settings[:infile] = body
    else
      settings[:postfields] = body.to_s
    end
  end

  settings[:headers] = {}
  request.headers.each do |key, values|
    settings[:headers][key] = values.join("\n")
  end
  settings[:protocols] = [:http, :https]
  settings[:redir_protocols] = [:http, :https]

  client = Typhoeus::Request.new(request.url, settings)
  client
end

#parse_curl_result(client) ⇒ Response

Parses the result of a curl call in a format that’s a bit more convenient to work with.

The method returns an array with the following elements:

* status - one of the 3 STATUS constants.
* curl_errno - A curl error number. Only set if status is
               STATUS_CURLERROR.
* curl_errmsg - A current error message. Only set if status is
                STATUS_CURLERROR.
* response - Response object. Only set if status is STATUS_SUCCESS, or
             STATUS_HTTPERROR.
* http_code - HTTP status code, as an int. Only set if Only set if
              status is STATUS_SUCCESS, or STATUS_HTTPERROR

Parameters:

  • client (Typhoeus::Request)

Returns:



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/tilia/http/client.rb', line 289

def parse_curl_result(client)
  client_response = client.response
  unless client_response.return_code == :ok
    return {
      'status'      => STATUS_CURLERROR,
      'curl_errno'  => client_response.return_code,
      'curl_errmsg' => client_response.return_message
    }
  end

  header_blob = client_response.response_headers
  # In the case of 204 No Content, strlen(response) == curl_info['header_size].
  # This will cause substr(response, curl_info['header_size']) return FALSE instead of NULL
  # An exception will be thrown when calling getBodyAsString then
  response_body = client_response.body
  response_body = nil if response_body == ''

  # In the case of 100 Continue, or redirects we'll have multiple lists
  # of headers for each separate HTTP response. We can easily split this
  # because they are separated by \r\n\r\n
  header_blob = header_blob.strip.split(/\r?\n\r?\n/)

  # We only care about the last set of headers
  header_blob = header_blob[-1]

  # Splitting headers
  header_blob = header_blob.split(/\r?\n/)

  response = Tilia::Http::Response.new
  response.status = client_response.code

  header_blob.each do |header|
    parts = header.split(':', 2)

    response.add_header(parts[0].strip, parts[1].strip) if parts.size == 2
  end

  response.body = response_body

  http_code = response.status.to_i

  {
    'status'    => http_code >= 400 ? STATUS_HTTPERROR : STATUS_SUCCESS,
    'response'  => response,
    'http_code' => http_code
  }
end

#pollBoolean

This method checks if any http requests have gotten results, and if so, call the appropriate success or error handlers.

This method will return true if there are still requests waiting to return, and false if all the work is done.

Returns:

  • (Boolean)


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
# File 'lib/tilia/http/client.rb', line 152

def poll
  # nothing to do?
  return false if @client_map.empty?

  # Hydra finishes them all
  @hydra.run

  @client_map.keys.each do |handler|
    (
      request,
      success_callback,
      error_callback,
      retry_count,
    ) = @client_map[handler]
    @client_map.delete handler

    curl_result = parse_curl_result(handler)
    do_retry = false

    if curl_result['status'] == STATUS_CURLERROR
      e = Exception.new

      box = Box.new(do_retry)
      emit('exception', [request, e, box, retry_count])
      do_retry = box.value

      if do_retry
        retry_count += 1
        send_async_internal(request, success_callback, error_callback, retry_count)
        next
      end

      curl_result['request'] = request

      error_callback.call(curl_result) if error_callback
    elsif curl_result['status'] == STATUS_HTTPERROR
      box = Box.new(do_retry)
      emit('error', [request, curl_result['response'], box, retry_count])
      emit("error:#{curl_result['http_code']}", [request, curl_result['response'], box, retry_count])
      do_retry = box.value

      if do_retry
        retry_count += 1
        send_async_internal(request, success_callback, error_callback, retry_count)
        next
      end

      curl_result['request'] = request

      error_callback.call(curl_result) if error_callback
    else
      emit('afterRequest', [request, curl_result['response']])

      success_callback.call(curl_result['response']) if success_callback
    end

    break if @client_map.empty?
  end

  @client_map.any?
end

#send_async(request, success = nil, error = nil) ⇒ void

This method returns an undefined value.

Sends a HTTP request asynchronously.

Due to the nature of PHP, you must from time to time poll to see if any new responses came in.

After calling sendAsync, you must therefore occasionally call the poll method, or wait.

Parameters:

  • request (RequestInterface)
  • success (#call) (defaults to: nil)
  • error (#call) (defaults to: nil)


138
139
140
141
142
143
# File 'lib/tilia/http/client.rb', line 138

def send_async(request, success = nil, error = nil)
  emit('beforeRequest', [request])

  send_async_internal(request, success, error)
  poll
end

#send_async_internal(request, success, error, retry_count = 0) ⇒ Object

Sends an asynchronous HTTP request.

We keep this in a separate method, so we can call it without triggering the beforeRequest event and don’t do the poll.

Parameters:

  • request (RequestInterface)
  • success (#call)
  • error (#call)
  • retry_count (Fixnum) (defaults to: 0)


346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/tilia/http/client.rb', line 346

def send_async_internal(request, success, error, retry_count = 0)
  @hydra = Typhoeus::Hydra.hydra unless @hydra

  client = create_client(request)
  @hydra.queue client

  @client_map[client] = [
    request,
    success,
    error,
    retry_count
  ]
end

#send_request(request) ⇒ ResponseInterface

Sends a request to a HTTP server, and returns a response.

Parameters:

Returns:



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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/tilia/http/client.rb', line 55

def send_request(request)
  emit('beforeRequest', [request])

  retry_count = 0
  redirects = 0

  response = nil
  code = 0

  loop do
    do_redirect = false
    do_retry = false

    begin
      response = do_request(request)

      code = response.status.to_i

      # We are doing in-PHP redirects, because curl's
      # FOLLOW_LOCATION throws errors when PHP is configured with
      # open_basedir.
      #
      # https://github.com/fruux/sabre-http/issues/12
      if [301, 302, 307, 308].include?(code) && redirects < @max_redirects
        old_location = request.url

        # Creating a new instance of the request object.
        request = request.clone

        # Setting the new location
        request.set_url(
          Tilia::Uri.resolve(
            old_location,
            response.header('Location')
          )
        )

        do_redirect = true
        redirects += 1
      end

      # This was a HTTP error
      if code >= 400
        box = Box.new(do_retry)
        emit('error', [request, response, box, retry_count])
        emit("error:#{code}", [request, response, box, retry_count])
        do_retry = box.value
      end
    rescue Tilia::Http::ClientException => e
      box = Box.new(do_retry)
      emit('exception', [request, e, box, retry_count])
      do_retry = box.value

      # If retry was still set to false, it means no event handler
      # dealt with the problem. In this case we just re-throw the
      # exception.
      raise e unless do_retry
    end

    retry_count += 1 if do_retry

    break unless do_retry || do_redirect
  end

  emit('afterRequest', [request, response])

  fail Tilia::Http::ClientHttpException.new(response), 'Oh oh' if @throw_exceptions && code >= 400

  response
end

#waitvoid

This method returns an undefined value.

Processes every HTTP request in the queue, and waits till they are all completed.



218
219
220
221
222
223
# File 'lib/tilia/http/client.rb', line 218

def wait
  loop do
    still_running = poll
    break unless still_running
  end
end