tilia-http 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.rubocop.yml +35 -0
- data/.simplecov +4 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.sabre.md +235 -0
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +69 -0
- data/LICENSE +27 -0
- data/LICENSE.sabre +27 -0
- data/README.md +68 -0
- data/Rakefile +17 -0
- data/examples/asyncclient.rb +45 -0
- data/examples/basicauth.rb +39 -0
- data/examples/client.rb +20 -0
- data/examples/reverseproxy.rb +39 -0
- data/examples/stringify.rb +37 -0
- data/lib/tilia/http/auth/abstract_auth.rb +51 -0
- data/lib/tilia/http/auth/aws.rb +191 -0
- data/lib/tilia/http/auth/basic.rb +43 -0
- data/lib/tilia/http/auth/bearer.rb +37 -0
- data/lib/tilia/http/auth/digest.rb +187 -0
- data/lib/tilia/http/auth.rb +12 -0
- data/lib/tilia/http/client.rb +452 -0
- data/lib/tilia/http/client_exception.rb +15 -0
- data/lib/tilia/http/client_http_exception.rb +37 -0
- data/lib/tilia/http/http_exception.rb +21 -0
- data/lib/tilia/http/message.rb +241 -0
- data/lib/tilia/http/message_decorator_trait.rb +183 -0
- data/lib/tilia/http/message_interface.rb +154 -0
- data/lib/tilia/http/request.rb +235 -0
- data/lib/tilia/http/request_decorator.rb +160 -0
- data/lib/tilia/http/request_interface.rb +126 -0
- data/lib/tilia/http/response.rb +164 -0
- data/lib/tilia/http/response_decorator.rb +58 -0
- data/lib/tilia/http/response_interface.rb +36 -0
- data/lib/tilia/http/sapi.rb +165 -0
- data/lib/tilia/http/url_util.rb +70 -0
- data/lib/tilia/http/util.rb +51 -0
- data/lib/tilia/http/version.rb +9 -0
- data/lib/tilia/http.rb +416 -0
- data/test/http/auth/aws_test.rb +189 -0
- data/test/http/auth/basic_test.rb +60 -0
- data/test/http/auth/bearer_test.rb +47 -0
- data/test/http/auth/digest_test.rb +141 -0
- data/test/http/client_mock.rb +101 -0
- data/test/http/client_test.rb +331 -0
- data/test/http/message_decorator_test.rb +67 -0
- data/test/http/message_test.rb +163 -0
- data/test/http/request_decorator_test.rb +87 -0
- data/test/http/request_test.rb +132 -0
- data/test/http/response_decorator_test.rb +28 -0
- data/test/http/response_test.rb +38 -0
- data/test/http/sapi_mock.rb +12 -0
- data/test/http/sapi_test.rb +133 -0
- data/test/http/url_util_test.rb +155 -0
- data/test/http/util_test.rb +186 -0
- data/test/http_test.rb +102 -0
- data/test/test_helper.rb +6 -0
- data/tilia-http.gemspec +18 -0
- metadata +192 -0
@@ -0,0 +1,452 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Http
|
3
|
+
# A rudimentary HTTP client.
|
4
|
+
#
|
5
|
+
# This object wraps PHP's curl extension and provides an easy way to send it a
|
6
|
+
# Request object, and return a Response object.
|
7
|
+
#
|
8
|
+
# This is by no means intended as the next best HTTP client, but it does the
|
9
|
+
# job and provides a simple integration with the rest of sabre/http.
|
10
|
+
#
|
11
|
+
# This client emits the following events:
|
12
|
+
# before_request(RequestInterface request)
|
13
|
+
# after_request(RequestInterface request, ResponseInterface response)
|
14
|
+
# error(RequestInterface request, ResponseInterface response, bool &retry, int retry_count)
|
15
|
+
# exception(RequestInterface request, ClientException e, bool &retry, int retry_count)
|
16
|
+
#
|
17
|
+
# The beforeRequest event allows you to do some last minute changes to the
|
18
|
+
# request before it's done, such as adding authentication headers.
|
19
|
+
#
|
20
|
+
# The afterRequest event will be emitted after the request is completed
|
21
|
+
# succesfully.
|
22
|
+
#
|
23
|
+
# If a HTTP error is returned (status code higher than 399) the error event is
|
24
|
+
# triggered. It's possible using this event to retry the request, by setting
|
25
|
+
# retry to true.
|
26
|
+
#
|
27
|
+
# The amount of times a request has retried is passed as retry_count, which
|
28
|
+
# can be used to avoid retrying indefinitely. The first time the event is
|
29
|
+
# called, this will be 0.
|
30
|
+
#
|
31
|
+
# It's also possible to intercept specific http errors, by subscribing to for
|
32
|
+
# example 'error:401'.
|
33
|
+
class Client < Tilia::Event::EventEmitter
|
34
|
+
protected
|
35
|
+
|
36
|
+
# List of curl settings
|
37
|
+
#
|
38
|
+
# @return array
|
39
|
+
attr_accessor :curl_settings
|
40
|
+
|
41
|
+
# Wether or not exceptions should be thrown when a HTTP error is returned.
|
42
|
+
#
|
43
|
+
# @return bool
|
44
|
+
attr_accessor :throw_exceptions
|
45
|
+
|
46
|
+
# The maximum number of times we'll follow a redirect.
|
47
|
+
#
|
48
|
+
# @return int
|
49
|
+
attr_accessor :max_redirects
|
50
|
+
|
51
|
+
public
|
52
|
+
|
53
|
+
# Initializes the client.
|
54
|
+
#
|
55
|
+
# @return [void]
|
56
|
+
def initialize
|
57
|
+
initialize_event_emitter_trait
|
58
|
+
|
59
|
+
@hydra = nil
|
60
|
+
@throw_exceptions = false
|
61
|
+
@max_redirects = 5
|
62
|
+
@hydra_settings = {
|
63
|
+
header: false, # RUBY otherwise header will be part of response.body
|
64
|
+
nobody: false
|
65
|
+
}
|
66
|
+
@client_map = {}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Sends a request to a HTTP server, and returns a response.
|
70
|
+
#
|
71
|
+
# @param RequestInterface request
|
72
|
+
# @return [ResponseInterface]
|
73
|
+
def send(request)
|
74
|
+
emit('beforeRequest', [request])
|
75
|
+
|
76
|
+
retry_count = 0
|
77
|
+
redirects = 0
|
78
|
+
|
79
|
+
response = nil
|
80
|
+
code = 0
|
81
|
+
|
82
|
+
loop do
|
83
|
+
do_redirect = false
|
84
|
+
do_retry = false
|
85
|
+
|
86
|
+
begin
|
87
|
+
response = do_request(request)
|
88
|
+
|
89
|
+
code = response.status.to_i
|
90
|
+
|
91
|
+
# We are doing in-PHP redirects, because curl's
|
92
|
+
# FOLLOW_LOCATION throws errors when PHP is configured with
|
93
|
+
# open_basedir.
|
94
|
+
#
|
95
|
+
# https://github.com/fruux/sabre-http/issues/12
|
96
|
+
if [301, 302, 307, 308].include?(code) && redirects < @max_redirects
|
97
|
+
old_location = request.url
|
98
|
+
|
99
|
+
# Creating a new instance of the request object.
|
100
|
+
request = request.clone
|
101
|
+
|
102
|
+
# Setting the new location
|
103
|
+
request.set_url(
|
104
|
+
Tilia::Uri.resolve(
|
105
|
+
old_location,
|
106
|
+
response.header('Location')
|
107
|
+
)
|
108
|
+
)
|
109
|
+
|
110
|
+
do_redirect = true
|
111
|
+
redirects += 1
|
112
|
+
end
|
113
|
+
|
114
|
+
# This was a HTTP error
|
115
|
+
if code >= 400
|
116
|
+
box = Box.new(do_retry)
|
117
|
+
emit('error', [request, response, box, retry_count])
|
118
|
+
emit("error:#{code}", [request, response, box, retry_count])
|
119
|
+
do_retry = box.value
|
120
|
+
end
|
121
|
+
rescue Tilia::Http::ClientException => e
|
122
|
+
box = Box.new(do_retry)
|
123
|
+
emit('exception', [request, e, box, retry_count])
|
124
|
+
do_retry = box.value
|
125
|
+
|
126
|
+
# If retry was still set to false, it means no event handler
|
127
|
+
# dealt with the problem. In this case we just re-throw the
|
128
|
+
# exception.
|
129
|
+
raise e unless do_retry
|
130
|
+
end
|
131
|
+
|
132
|
+
retry_count += 1 if do_retry
|
133
|
+
|
134
|
+
break unless do_retry || do_redirect
|
135
|
+
end
|
136
|
+
|
137
|
+
emit('afterRequest', [request, response])
|
138
|
+
|
139
|
+
fail Tilia::Http::ClientHttpException.new(response), 'Oh oh' if @throw_exceptions && code >= 400
|
140
|
+
|
141
|
+
response
|
142
|
+
end
|
143
|
+
|
144
|
+
# Sends a HTTP request asynchronously.
|
145
|
+
#
|
146
|
+
# Due to the nature of PHP, you must from time to time poll to see if any
|
147
|
+
# new responses came in.
|
148
|
+
#
|
149
|
+
# After calling sendAsync, you must therefore occasionally call the poll
|
150
|
+
# method, or wait.
|
151
|
+
#
|
152
|
+
# @param RequestInterface request
|
153
|
+
# @param callable success
|
154
|
+
# @param callable error
|
155
|
+
# @return [void]
|
156
|
+
def send_async(request, success = nil, error = nil)
|
157
|
+
emit('beforeRequest', [request])
|
158
|
+
|
159
|
+
send_async_internal(request, success, error)
|
160
|
+
poll
|
161
|
+
end
|
162
|
+
|
163
|
+
# This method checks if any http requests have gotten results, and if so,
|
164
|
+
# call the appropriate success or error handlers.
|
165
|
+
#
|
166
|
+
# This method will return true if there are still requests waiting to
|
167
|
+
# return, and false if all the work is done.
|
168
|
+
#
|
169
|
+
# @return bool
|
170
|
+
def poll
|
171
|
+
# nothing to do?
|
172
|
+
return false if @client_map.empty?
|
173
|
+
|
174
|
+
# Hydra finishes them all
|
175
|
+
@hydra.run
|
176
|
+
|
177
|
+
@client_map.keys.each do |handler|
|
178
|
+
(
|
179
|
+
request,
|
180
|
+
success_callback,
|
181
|
+
error_callback,
|
182
|
+
retry_count,
|
183
|
+
) = @client_map[handler]
|
184
|
+
@client_map.delete handler
|
185
|
+
|
186
|
+
curl_result = parse_curl_result(handler)
|
187
|
+
do_retry = false
|
188
|
+
|
189
|
+
if curl_result['status'] == self.class::STATUS_CURLERROR
|
190
|
+
e = Exception.new
|
191
|
+
|
192
|
+
box = Box.new(do_retry)
|
193
|
+
emit('exception', [request, e, box, retry_count])
|
194
|
+
do_retry = box.value
|
195
|
+
|
196
|
+
if do_retry
|
197
|
+
retry_count += 1
|
198
|
+
send_async_internal(request, success_callback, error_callback, retry_count)
|
199
|
+
next
|
200
|
+
end
|
201
|
+
|
202
|
+
curl_result['request'] = request
|
203
|
+
|
204
|
+
error_callback.call(curl_result) if error_callback
|
205
|
+
elsif curl_result['status'] == self.class::STATUS_HTTPERROR
|
206
|
+
box = Box.new(do_retry)
|
207
|
+
emit('error', [request, curl_result['response'], box, retry_count])
|
208
|
+
emit("error:#{curl_result['http_code']}", [request, curl_result['response'], box, retry_count])
|
209
|
+
do_retry = box.value
|
210
|
+
|
211
|
+
if do_retry
|
212
|
+
retry_count += 1
|
213
|
+
send_async_internal(request, success_callback, error_callback, retry_count)
|
214
|
+
next
|
215
|
+
end
|
216
|
+
|
217
|
+
curl_result['request'] = request
|
218
|
+
|
219
|
+
error_callback.call(curl_result) if error_callback
|
220
|
+
else
|
221
|
+
emit('afterRequest', [request, curl_result['response']])
|
222
|
+
|
223
|
+
success_callback.call(curl_result['response']) if success_callback
|
224
|
+
end
|
225
|
+
|
226
|
+
break if @client_map.empty?
|
227
|
+
end
|
228
|
+
|
229
|
+
@client_map.any?
|
230
|
+
end
|
231
|
+
|
232
|
+
# Processes every HTTP request in the queue, and waits till they are all
|
233
|
+
# completed.
|
234
|
+
#
|
235
|
+
# @return [void]
|
236
|
+
def wait
|
237
|
+
loop do
|
238
|
+
still_running = poll
|
239
|
+
break unless still_running
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# If this is set to true, the Client will automatically throw exceptions
|
244
|
+
# upon HTTP errors.
|
245
|
+
#
|
246
|
+
# This means that if a response came back with a status code greater than
|
247
|
+
# or equal to 400, we will throw a ClientHttpException.
|
248
|
+
#
|
249
|
+
# This only works for the send method. Throwing exceptions for
|
250
|
+
# send_async is not supported.
|
251
|
+
#
|
252
|
+
# @param bool throw_exceptions
|
253
|
+
# @return [void]
|
254
|
+
attr_writer :throw_exceptions
|
255
|
+
|
256
|
+
# Adds a CURL setting.
|
257
|
+
#
|
258
|
+
# These settings will be included in every HTTP request.
|
259
|
+
#
|
260
|
+
# @param int name
|
261
|
+
# @param mixed value
|
262
|
+
# @return [void]
|
263
|
+
def add_curl_setting(name, value)
|
264
|
+
@hydra_settings[name] = value
|
265
|
+
end
|
266
|
+
|
267
|
+
protected
|
268
|
+
|
269
|
+
# This method is responsible for performing a single request.
|
270
|
+
#
|
271
|
+
# @param RequestInterface request
|
272
|
+
# @return [ResponseInterface]
|
273
|
+
def do_request(request)
|
274
|
+
client = create_client(request)
|
275
|
+
client.run
|
276
|
+
|
277
|
+
response = parse_curl_result(client)
|
278
|
+
|
279
|
+
if response['status'] == self.class::STATUS_CURLERROR
|
280
|
+
fail Tilia::Http::ClientException.new(response['curl_errno']), response['curl_errmsg']
|
281
|
+
end
|
282
|
+
|
283
|
+
response['response']
|
284
|
+
end
|
285
|
+
|
286
|
+
protected
|
287
|
+
|
288
|
+
# Cached curl handle.
|
289
|
+
#
|
290
|
+
# By keeping this resource around for the lifetime of this object, things
|
291
|
+
# like persistent connections are possible.
|
292
|
+
#
|
293
|
+
# @return resource
|
294
|
+
attr_accessor :curl_handle
|
295
|
+
|
296
|
+
# Handler for curl_multi requests.
|
297
|
+
#
|
298
|
+
# The first time sendAsync is used, this will be created.
|
299
|
+
#
|
300
|
+
# @return resource
|
301
|
+
attr_accessor :curl_multi_handle
|
302
|
+
|
303
|
+
# Has a list of curl handles, as well as their associated success and
|
304
|
+
# error callbacks.
|
305
|
+
#
|
306
|
+
# @return array
|
307
|
+
attr_accessor :curl_multi_map
|
308
|
+
|
309
|
+
public
|
310
|
+
|
311
|
+
STATUS_SUCCESS = 0
|
312
|
+
STATUS_CURLERROR = 1
|
313
|
+
STATUS_HTTPERROR = 2
|
314
|
+
|
315
|
+
# Parses the result of a curl call in a format that's a bit more
|
316
|
+
# convenient to work with.
|
317
|
+
#
|
318
|
+
# The method returns an array with the following elements:
|
319
|
+
# * status - one of the 3 STATUS constants.
|
320
|
+
# * curl_errno - A curl error number. Only set if status is
|
321
|
+
# STATUS_CURLERROR.
|
322
|
+
# * curl_errmsg - A current error message. Only set if status is
|
323
|
+
# STATUS_CURLERROR.
|
324
|
+
# * response - Response object. Only set if status is STATUS_SUCCESS, or
|
325
|
+
# STATUS_HTTPERROR.
|
326
|
+
# * http_code - HTTP status code, as an int. Only set if Only set if
|
327
|
+
# status is STATUS_SUCCESS, or STATUS_HTTPERROR
|
328
|
+
#
|
329
|
+
# @param [String] response
|
330
|
+
# @param resource curl_handle
|
331
|
+
# @return Response
|
332
|
+
def parse_curl_result(client)
|
333
|
+
client_response = client.response
|
334
|
+
unless client_response.return_code == :ok
|
335
|
+
return {
|
336
|
+
'status' => self.class::STATUS_CURLERROR,
|
337
|
+
'curl_errno' => client_response.return_code,
|
338
|
+
'curl_errmsg' => client_response.return_message
|
339
|
+
}
|
340
|
+
end
|
341
|
+
|
342
|
+
header_blob = client_response.response_headers
|
343
|
+
# In the case of 204 No Content, strlen(response) == curl_info['header_size].
|
344
|
+
# This will cause substr(response, curl_info['header_size']) return FALSE instead of NULL
|
345
|
+
# An exception will be thrown when calling getBodyAsString then
|
346
|
+
response_body = client_response.body
|
347
|
+
response_body = nil if response_body == ''
|
348
|
+
|
349
|
+
# In the case of 100 Continue, or redirects we'll have multiple lists
|
350
|
+
# of headers for each separate HTTP response. We can easily split this
|
351
|
+
# because they are separated by \r\n\r\n
|
352
|
+
header_blob = header_blob.strip.split(/\r?\n\r?\n/)
|
353
|
+
|
354
|
+
# We only care about the last set of headers
|
355
|
+
header_blob = header_blob[-1]
|
356
|
+
|
357
|
+
# Splitting headers
|
358
|
+
header_blob = header_blob.split(/\r?\n/)
|
359
|
+
|
360
|
+
response = Tilia::Http::Response.new
|
361
|
+
response.status = client_response.code
|
362
|
+
|
363
|
+
header_blob.each do |header|
|
364
|
+
parts = header.split(':', 2)
|
365
|
+
|
366
|
+
response.add_header(parts[0].strip, parts[1].strip) if parts.size == 2
|
367
|
+
end
|
368
|
+
|
369
|
+
response.body = response_body
|
370
|
+
|
371
|
+
http_code = response.status.to_i
|
372
|
+
|
373
|
+
{
|
374
|
+
'status' => http_code >= 400 ? self.class::STATUS_HTTPERROR : self.class::STATUS_SUCCESS,
|
375
|
+
'response' => response,
|
376
|
+
'http_code' => http_code
|
377
|
+
}
|
378
|
+
end
|
379
|
+
|
380
|
+
# Sends an asynchronous HTTP request.
|
381
|
+
#
|
382
|
+
# We keep this in a separate method, so we can call it without triggering
|
383
|
+
# the beforeRequest event and don't do the poll.
|
384
|
+
#
|
385
|
+
# @param RequestInterface request
|
386
|
+
# @param callable success
|
387
|
+
# @param callable error
|
388
|
+
# @param int retry_count
|
389
|
+
def send_async_internal(request, success, error, retry_count = 0)
|
390
|
+
@hydra = Typhoeus::Hydra.hydra unless @hydra
|
391
|
+
|
392
|
+
client = create_client(request)
|
393
|
+
@hydra.queue client
|
394
|
+
|
395
|
+
@client_map[client] = [
|
396
|
+
request,
|
397
|
+
success,
|
398
|
+
error,
|
399
|
+
retry_count
|
400
|
+
]
|
401
|
+
end
|
402
|
+
|
403
|
+
# TODO: document
|
404
|
+
def create_client(request)
|
405
|
+
settings = {}
|
406
|
+
@hydra_settings.each do |key, value|
|
407
|
+
settings[key] = value
|
408
|
+
end
|
409
|
+
|
410
|
+
case request.method
|
411
|
+
when 'HEAD'
|
412
|
+
settings[:nobody] = true
|
413
|
+
settings[:method] = :head
|
414
|
+
settings[:postfields] = ''
|
415
|
+
settings[:put] = false
|
416
|
+
when 'GET'
|
417
|
+
settings[:method] = :get
|
418
|
+
settings[:postfields] = ''
|
419
|
+
settings[:put] = false
|
420
|
+
else
|
421
|
+
settings[:method] = request.method.downcase.to_sym
|
422
|
+
body = request.body
|
423
|
+
if !body.is_a?(String) && !body.nil?
|
424
|
+
settings[:put] = true
|
425
|
+
settings[:infile] = body
|
426
|
+
else
|
427
|
+
settings[:postfields] = body.to_s
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
settings[:headers] = {}
|
432
|
+
request.headers.each do |key, values|
|
433
|
+
settings[:headers][key] = values.join("\n")
|
434
|
+
end
|
435
|
+
settings[:protocols] = [:http, :https]
|
436
|
+
settings[:redir_protocols] = [:http, :https]
|
437
|
+
|
438
|
+
client = Typhoeus::Request.new(request.url, settings)
|
439
|
+
client
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# Class to fix rubys real-world "pass-by-reference" shortcommings
|
444
|
+
class Box
|
445
|
+
attr_accessor :value
|
446
|
+
|
447
|
+
def initialize(v = nil)
|
448
|
+
@value = v
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Http
|
3
|
+
# This exception may be emitted by the HTTP\Client class, in case there was a
|
4
|
+
# problem emitting the request.
|
5
|
+
class ClientException < Exception
|
6
|
+
# TODO: document
|
7
|
+
def initialize(code)
|
8
|
+
@code = code.to_i
|
9
|
+
end
|
10
|
+
|
11
|
+
# TODO: document
|
12
|
+
attr_accessor :code
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Http
|
3
|
+
# This exception represents a HTTP error coming from the Client.
|
4
|
+
#
|
5
|
+
# By default the Client will not emit these, this has to be explicitly enabled
|
6
|
+
# with the setThrowExceptions method.
|
7
|
+
class ClientHttpException < Tilia::Http::HttpException
|
8
|
+
protected
|
9
|
+
|
10
|
+
# Response object
|
11
|
+
#
|
12
|
+
# @return [ResponseInterface]
|
13
|
+
attr_accessor :response
|
14
|
+
|
15
|
+
public
|
16
|
+
|
17
|
+
# Constructor
|
18
|
+
#
|
19
|
+
# @param ResponseInterface response
|
20
|
+
def initialize(response)
|
21
|
+
@response = response
|
22
|
+
end
|
23
|
+
|
24
|
+
# The http status code for the error.
|
25
|
+
#
|
26
|
+
# @return int
|
27
|
+
def http_status
|
28
|
+
@response.status
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the full response object.
|
32
|
+
#
|
33
|
+
# @return [ResponseInterface]
|
34
|
+
attr_reader :response
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Tilia
|
2
|
+
module Http
|
3
|
+
# An exception representing a HTTP error.
|
4
|
+
#
|
5
|
+
# This can be used as a generic exception in your application, if you'd like
|
6
|
+
# to map HTTP errors to exceptions.
|
7
|
+
#
|
8
|
+
# If you'd like to use this, create a new exception class, extending Exception
|
9
|
+
# and implementing this interface.
|
10
|
+
class HttpException < Exception
|
11
|
+
# The http status code for the error.
|
12
|
+
#
|
13
|
+
# This may either be just the number, or a number and a human-readable
|
14
|
+
# message, separated by a space.
|
15
|
+
#
|
16
|
+
# @return [String, nil]
|
17
|
+
def http_status
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|