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.



56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/tilia/http/client.rb', line 56

def initialize
  initialize_event_emitter_trait

  @hydra = nil
  @throw_exceptions = false
  @max_redirects = 5
  @curl_settings = {
    header: false, # RUBY otherwise header will be part of response.body
    nobody: false
  }
  @client_map = {}
end

Instance Attribute Details

#throw_exceptions=(value) ⇒ void

This method returns an undefined value.

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.

Parameters:

  • bool

    throw_exceptions



44
45
46
# File 'lib/tilia/http/client.rb', line 44

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:

  • int

    name

  • mixed

    value



263
264
265
# File 'lib/tilia/http/client.rb', line 263

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

#create_client(request) ⇒ Object

TODO: document



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'lib/tilia/http/client.rb', line 404

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) ⇒ Object

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:

  • response (String)
  • resource

    curl_handle

Returns:

  • Response



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/tilia/http/client.rb', line 332

def parse_curl_result(client)
  client_response = client.response
  unless client_response.return_code == :ok
    return {
      'status'      => self.class::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 ? self.class::STATUS_HTTPERROR : self.class::STATUS_SUCCESS,
    'response'  => response,
    'http_code' => http_code
  }
end

#pollObject

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:

  • bool



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

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'] == self.class::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'] == self.class::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:

  • RequestInterface

    request

  • callable

    success

  • callable

    error



156
157
158
159
160
161
# File 'lib/tilia/http/client.rb', line 156

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:

  • RequestInterface

    request

  • callable

    success

  • callable

    error

  • int

    retry_count



389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/tilia/http/client.rb', line 389

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:

  • RequestInterface

    request

Returns:



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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/tilia/http/client.rb', line 73

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.



236
237
238
239
240
241
# File 'lib/tilia/http/client.rb', line 236

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