http 5.0.0.pre3 → 5.0.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 +4 -4
- data/.github/workflows/ci.yml +65 -0
- data/.gitignore +6 -10
- data/.rspec +0 -4
- data/.rubocop.yml +7 -124
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop_todo.yml +192 -0
- data/CHANGES.md +48 -1
- data/Gemfile +18 -11
- data/README.md +13 -17
- data/Rakefile +2 -10
- data/http.gemspec +2 -2
- data/lib/http/chainable.rb +15 -14
- data/lib/http/client.rb +18 -11
- data/lib/http/connection.rb +7 -3
- data/lib/http/content_type.rb +10 -5
- data/lib/http/feature.rb +1 -1
- data/lib/http/features/instrumentation.rb +1 -1
- data/lib/http/features/logging.rb +19 -21
- data/lib/http/headers.rb +3 -3
- data/lib/http/mime_type/adapter.rb +2 -0
- data/lib/http/options.rb +2 -2
- data/lib/http/redirector.rb +1 -1
- data/lib/http/request.rb +7 -4
- data/lib/http/response/body.rb +1 -2
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +72 -64
- data/lib/http/response/status.rb +2 -2
- data/lib/http/timeout/global.rb +16 -28
- data/lib/http/timeout/null.rb +2 -1
- data/lib/http/timeout/per_operation.rb +31 -55
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +75 -41
- data/spec/lib/http/features/instrumentation_spec.rb +21 -15
- data/spec/lib/http/features/logging_spec.rb +2 -4
- data/spec/lib/http/headers_spec.rb +3 -3
- data/spec/lib/http/response/parser_spec.rb +2 -2
- data/spec/lib/http_spec.rb +20 -2
- data/spec/spec_helper.rb +21 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +14 -2
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/simplecov.rb +19 -0
- metadata +21 -16
- data/.coveralls.yml +0 -1
- data/.travis.yml +0 -38
data/lib/http/timeout/null.rb
CHANGED
@@ -12,7 +12,7 @@ module HTTP
|
|
12
12
|
|
13
13
|
attr_reader :options, :socket
|
14
14
|
|
15
|
-
def initialize(options = {})
|
15
|
+
def initialize(options = {})
|
16
16
|
@options = options
|
17
17
|
end
|
18
18
|
|
@@ -36,6 +36,7 @@ module HTTP
|
|
36
36
|
connect_ssl
|
37
37
|
|
38
38
|
return unless ssl_context.verify_mode == OpenSSL::SSL::VERIFY_PEER
|
39
|
+
return if ssl_context.respond_to?(:verify_hostname) && !ssl_context.verify_hostname
|
39
40
|
|
40
41
|
@socket.post_connection_check(host)
|
41
42
|
end
|
@@ -34,66 +34,42 @@ module HTTP
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
:eof
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
# Read data from the socket
|
60
|
-
def readpartial(size, buffer = nil)
|
61
|
-
timeout = false
|
62
|
-
loop do
|
63
|
-
result = @socket.read_nonblock(size, buffer, :exception => false)
|
64
|
-
|
65
|
-
return :eof if result.nil?
|
66
|
-
return result if result != :wait_readable
|
67
|
-
|
68
|
-
raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
|
69
|
-
|
70
|
-
# marking the socket for timeout. Why is this not being raised immediately?
|
71
|
-
# it seems there is some race-condition on the network level between calling
|
72
|
-
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
|
73
|
-
# for reads, and when waiting for x seconds, it returns nil suddenly without completing
|
74
|
-
# the x seconds. In a normal case this would be a timeout on wait/read, but it can
|
75
|
-
# also mean that the socket has been closed by the server. Therefore we "mark" the
|
76
|
-
# socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
|
77
|
-
# timeout. Else, the first timeout was a proper timeout.
|
78
|
-
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
|
79
|
-
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
|
80
|
-
timeout = true unless @socket.to_io.wait_readable(@read_timeout)
|
81
|
-
end
|
37
|
+
# Read data from the socket
|
38
|
+
def readpartial(size, buffer = nil)
|
39
|
+
timeout = false
|
40
|
+
loop do
|
41
|
+
result = @socket.read_nonblock(size, buffer, :exception => false)
|
42
|
+
|
43
|
+
return :eof if result.nil?
|
44
|
+
return result if result != :wait_readable
|
45
|
+
|
46
|
+
raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
|
47
|
+
|
48
|
+
# marking the socket for timeout. Why is this not being raised immediately?
|
49
|
+
# it seems there is some race-condition on the network level between calling
|
50
|
+
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
|
51
|
+
# for reads, and when waiting for x seconds, it returns nil suddenly without completing
|
52
|
+
# the x seconds. In a normal case this would be a timeout on wait/read, but it can
|
53
|
+
# also mean that the socket has been closed by the server. Therefore we "mark" the
|
54
|
+
# socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
|
55
|
+
# timeout. Else, the first timeout was a proper timeout.
|
56
|
+
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
|
57
|
+
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
|
58
|
+
timeout = true unless @socket.to_io.wait_readable(@read_timeout)
|
82
59
|
end
|
60
|
+
end
|
83
61
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
62
|
+
# Write data to the socket
|
63
|
+
def write(data)
|
64
|
+
timeout = false
|
65
|
+
loop do
|
66
|
+
result = @socket.write_nonblock(data, :exception => false)
|
67
|
+
return result unless result == :wait_writable
|
90
68
|
|
91
|
-
|
69
|
+
raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
|
92
70
|
|
93
|
-
|
94
|
-
end
|
71
|
+
timeout = true unless @socket.to_io.wait_writable(@write_timeout)
|
95
72
|
end
|
96
|
-
|
97
73
|
end
|
98
74
|
end
|
99
75
|
end
|
data/lib/http/version.rb
CHANGED
@@ -8,46 +8,50 @@ require "support/ssl_helper"
|
|
8
8
|
RSpec.describe HTTP::Client do
|
9
9
|
run_server(:dummy) { DummyServer.new }
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
def stubs
|
18
|
-
@stubs ||= {}
|
19
|
-
end
|
11
|
+
before do
|
12
|
+
stubbed_client = Class.new(HTTP::Client) do
|
13
|
+
def perform(request, options)
|
14
|
+
stubbed = stubs[HTTP::URI::NORMALIZER.call(request.uri).to_s]
|
15
|
+
stubbed ? stubbed.call(request) : super(request, options)
|
16
|
+
end
|
20
17
|
|
21
|
-
|
22
|
-
|
23
|
-
o[HTTP::URI.parse k] = v
|
18
|
+
def stubs
|
19
|
+
@stubs ||= {}
|
24
20
|
end
|
25
21
|
|
26
|
-
|
22
|
+
def stub(stubs)
|
23
|
+
@stubs = stubs.transform_keys do |k|
|
24
|
+
HTTP::URI::NORMALIZER.call(k).to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
self
|
28
|
+
end
|
27
29
|
end
|
28
|
-
end
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
def redirect_response(location, status = 302)
|
32
|
+
lambda do |request|
|
33
|
+
HTTP::Response.new(
|
34
|
+
:status => status,
|
35
|
+
:version => "1.1",
|
36
|
+
:headers => {"Location" => location},
|
37
|
+
:body => "",
|
38
|
+
:request => request
|
39
|
+
)
|
40
|
+
end
|
39
41
|
end
|
40
|
-
end
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
43
|
+
def simple_response(body, status = 200)
|
44
|
+
lambda do |request|
|
45
|
+
HTTP::Response.new(
|
46
|
+
:status => status,
|
47
|
+
:version => "1.1",
|
48
|
+
:body => body,
|
49
|
+
:request => request
|
50
|
+
)
|
51
|
+
end
|
50
52
|
end
|
53
|
+
|
54
|
+
stub_const("StubbedClient", stubbed_client)
|
51
55
|
end
|
52
56
|
|
53
57
|
describe "following redirects" do
|
@@ -105,9 +109,8 @@ RSpec.describe HTTP::Client do
|
|
105
109
|
end
|
106
110
|
|
107
111
|
it "works like a charm in real world" do
|
108
|
-
|
109
|
-
|
110
|
-
expect(client.get(url).to_s).to include "support for non-ascii URIs"
|
112
|
+
expect(HTTP.follow.get("https://bit.ly/2UaBT4R").parse(:json)).
|
113
|
+
to include("url" => "https://httpbin.org/anything/könig")
|
111
114
|
end
|
112
115
|
end
|
113
116
|
end
|
@@ -197,6 +200,22 @@ RSpec.describe HTTP::Client do
|
|
197
200
|
|
198
201
|
client.get("http://example.com/", :form => {:foo => HTTP::FormData::Part.new("content")})
|
199
202
|
end
|
203
|
+
|
204
|
+
context "when passing an HTTP::FormData object directly" do
|
205
|
+
it "creates url encoded form data object" do
|
206
|
+
client = HTTP::Client.new
|
207
|
+
form_data = HTTP::FormData::Multipart.new({ :foo => "bar" })
|
208
|
+
|
209
|
+
allow(client).to receive(:perform)
|
210
|
+
|
211
|
+
expect(HTTP::Request).to receive(:new) do |opts|
|
212
|
+
expect(opts[:body]).to be form_data
|
213
|
+
expect(opts[:body].to_s).to match(/^Content-Disposition: form-data; name="foo"\r\n\r\nbar\r\n/m)
|
214
|
+
end
|
215
|
+
|
216
|
+
client.get("http://example.com/", :form => form_data)
|
217
|
+
end
|
218
|
+
end
|
200
219
|
end
|
201
220
|
|
202
221
|
describe "passing json" do
|
@@ -220,9 +239,9 @@ RSpec.describe HTTP::Client do
|
|
220
239
|
end
|
221
240
|
|
222
241
|
it "works like a charm in real world" do
|
223
|
-
url
|
224
|
-
|
225
|
-
expect(
|
242
|
+
url = "https://httpbin.org/anything/ö無"
|
243
|
+
|
244
|
+
expect(HTTP.follow.get(url).parse(:json)).to include("url" => url)
|
226
245
|
end
|
227
246
|
end
|
228
247
|
|
@@ -291,6 +310,7 @@ RSpec.describe HTTP::Client do
|
|
291
310
|
end
|
292
311
|
end
|
293
312
|
end
|
313
|
+
|
294
314
|
it "is given a chance to wrap the Request" do
|
295
315
|
feature_instance = feature_class.new
|
296
316
|
|
@@ -299,7 +319,7 @@ RSpec.describe HTTP::Client do
|
|
299
319
|
|
300
320
|
expect(response.code).to eq(200)
|
301
321
|
expect(feature_instance.captured_request.verb).to eq(:get)
|
302
|
-
expect(feature_instance.captured_request.uri.to_s).to eq(dummy.endpoint
|
322
|
+
expect(feature_instance.captured_request.uri.to_s).to eq("#{dummy.endpoint}/")
|
303
323
|
end
|
304
324
|
|
305
325
|
it "is given a chance to wrap the Response" do
|
@@ -325,6 +345,19 @@ RSpec.describe HTTP::Client do
|
|
325
345
|
expect(feature_instance.captured_request.verb).to eq(:post)
|
326
346
|
expect(feature_instance.captured_request.uri.to_s).to eq(sleep_url)
|
327
347
|
end
|
348
|
+
|
349
|
+
it "is given a chance to handle a connection timeout error" do
|
350
|
+
allow(TCPSocket).to receive(:open) { sleep 1 }
|
351
|
+
sleep_url = "#{dummy.endpoint}/sleep"
|
352
|
+
feature_instance = feature_class.new
|
353
|
+
|
354
|
+
expect do
|
355
|
+
client.use(:test_feature => feature_instance).
|
356
|
+
timeout(0.001).
|
357
|
+
request(:post, sleep_url)
|
358
|
+
end.to raise_error(HTTP::TimeoutError)
|
359
|
+
expect(feature_instance.captured_error).to be_a(HTTP::TimeoutError)
|
360
|
+
end
|
328
361
|
end
|
329
362
|
end
|
330
363
|
|
@@ -335,7 +368,8 @@ RSpec.describe HTTP::Client do
|
|
335
368
|
let(:client) { described_class.new(options.merge(extra_options)) }
|
336
369
|
end
|
337
370
|
|
338
|
-
|
371
|
+
# TODO: https://github.com/httprb/http/issues/627
|
372
|
+
xdescribe "working with SSL" do
|
339
373
|
run_server(:dummy_ssl) { DummyServer.new(:ssl => true) }
|
340
374
|
|
341
375
|
let(:extra_options) { {} }
|
@@ -476,7 +510,7 @@ RSpec.describe HTTP::Client do
|
|
476
510
|
BODY
|
477
511
|
end
|
478
512
|
|
479
|
-
|
513
|
+
xit "raises HTTP::ConnectionError" do
|
480
514
|
expect { client.get(dummy.endpoint).to_s }.to raise_error(HTTP::ConnectionError)
|
481
515
|
end
|
482
516
|
end
|
@@ -2,8 +2,29 @@
|
|
2
2
|
|
3
3
|
RSpec.describe HTTP::Features::Instrumentation do
|
4
4
|
subject(:feature) { HTTP::Features::Instrumentation.new(:instrumenter => instrumenter) }
|
5
|
+
|
5
6
|
let(:instrumenter) { TestInstrumenter.new }
|
6
7
|
|
8
|
+
before do
|
9
|
+
test_instrumenter = Class.new(HTTP::Features::Instrumentation::NullInstrumenter) do
|
10
|
+
attr_reader :output
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@output = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def start(_name, payload)
|
17
|
+
output[:start] = payload
|
18
|
+
end
|
19
|
+
|
20
|
+
def finish(_name, payload)
|
21
|
+
output[:finish] = payload
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
stub_const("TestInstrumenter", test_instrumenter)
|
26
|
+
end
|
27
|
+
|
7
28
|
describe "logging the request" do
|
8
29
|
let(:request) do
|
9
30
|
HTTP::Request.new(
|
@@ -39,19 +60,4 @@ RSpec.describe HTTP::Features::Instrumentation do
|
|
39
60
|
expect(instrumenter.output[:finish]).to eq(:response => response)
|
40
61
|
end
|
41
62
|
end
|
42
|
-
|
43
|
-
class TestInstrumenter < HTTP::Features::Instrumentation::NullInstrumenter
|
44
|
-
attr_reader :output
|
45
|
-
def initialize
|
46
|
-
@output = {}
|
47
|
-
end
|
48
|
-
|
49
|
-
def start(_name, payload)
|
50
|
-
output[:start] = payload
|
51
|
-
end
|
52
|
-
|
53
|
-
def finish(_name, payload)
|
54
|
-
output[:finish] = payload
|
55
|
-
end
|
56
|
-
end
|
57
63
|
end
|
@@ -4,10 +4,8 @@ require "logger"
|
|
4
4
|
|
5
5
|
RSpec.describe HTTP::Features::Logging do
|
6
6
|
subject(:feature) do
|
7
|
-
logger
|
8
|
-
logger.formatter = ->(severity, _, _, message)
|
9
|
-
format("** %s **\n%s\n", severity, message)
|
10
|
-
end
|
7
|
+
logger = Logger.new(logdev)
|
8
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
11
9
|
|
12
10
|
described_class.new(:logger => logger)
|
13
11
|
end
|
@@ -321,17 +321,17 @@ RSpec.describe HTTP::Headers do
|
|
321
321
|
|
322
322
|
it "yields header keys specified as symbols in normalized form" do
|
323
323
|
keys = headers.each.map(&:first)
|
324
|
-
expect(keys).to eq([
|
324
|
+
expect(keys).to eq(%w[Set-Cookie Content-Type Set-Cookie])
|
325
325
|
end
|
326
326
|
|
327
327
|
it "yields headers specified as strings without conversion" do
|
328
328
|
headers.add "X_kEy", "value"
|
329
329
|
keys = headers.each.map(&:first)
|
330
|
-
expect(keys).to eq([
|
330
|
+
expect(keys).to eq(%w[Set-Cookie Content-Type Set-Cookie X_kEy])
|
331
331
|
end
|
332
332
|
|
333
333
|
it "returns self instance if block given" do
|
334
|
-
expect(headers.each { |*| }).to be headers
|
334
|
+
expect(headers.each { |*| }).to be headers # rubocop:disable Lint/EmptyBlock
|
335
335
|
end
|
336
336
|
|
337
337
|
it "returns Enumerator if no block given" do
|
@@ -32,7 +32,7 @@ RSpec.describe HTTP::Response::Parser do
|
|
32
32
|
end
|
33
33
|
|
34
34
|
context "response in many parts" do
|
35
|
-
let(:parts) { raw_response.
|
35
|
+
let(:parts) { raw_response.chars }
|
36
36
|
|
37
37
|
it "parses headers" do
|
38
38
|
expect(subject.headers.to_h).to eq(expected_headers)
|
@@ -62,7 +62,7 @@ RSpec.describe HTTP::Response::Parser do
|
|
62
62
|
end
|
63
63
|
|
64
64
|
context "when response is feeded in many parts" do
|
65
|
-
let(:parts) { raw_response.
|
65
|
+
let(:parts) { raw_response.chars }
|
66
66
|
|
67
67
|
it "skips to next non-info response" do
|
68
68
|
expect(subject.status_code).to eq(200)
|
data/spec/lib/http_spec.rb
CHANGED
@@ -95,7 +95,8 @@ RSpec.describe HTTP do
|
|
95
95
|
expect(response.to_s).to match(/<!doctype html>/)
|
96
96
|
end
|
97
97
|
|
98
|
-
|
98
|
+
# TODO: htt:s://github.com/httprb/http/issues/627
|
99
|
+
xcontext "ssl" do
|
99
100
|
it "responds with the endpoint's body" do
|
100
101
|
response = ssl_client.via(proxy.addr, proxy.port).get dummy_ssl.endpoint
|
101
102
|
expect(response.to_s).to match(/<!doctype html>/)
|
@@ -131,7 +132,8 @@ RSpec.describe HTTP do
|
|
131
132
|
expect(response.status).to eq(407)
|
132
133
|
end
|
133
134
|
|
134
|
-
|
135
|
+
# TODO: htt:s://github.com/httprb/http/issues/627
|
136
|
+
xcontext "ssl" do
|
135
137
|
it "responds with the endpoint's body" do
|
136
138
|
response = ssl_client.via(proxy.addr, proxy.port, "username", "password").get dummy_ssl.endpoint
|
137
139
|
expect(response.to_s).to match(/<!doctype html>/)
|
@@ -438,6 +440,22 @@ RSpec.describe HTTP do
|
|
438
440
|
|
439
441
|
expect(response.to_s).to eq("#{body}-deflated")
|
440
442
|
end
|
443
|
+
|
444
|
+
it "returns empty body for no content response where Content-Encoding is gzip" do
|
445
|
+
client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "gzip")
|
446
|
+
body = "Hello!"
|
447
|
+
response = client.post("#{dummy.endpoint}/no-content-204", :body => body)
|
448
|
+
|
449
|
+
expect(response.to_s).to eq("")
|
450
|
+
end
|
451
|
+
|
452
|
+
it "returns empty body for no content response where Content-Encoding is deflate" do
|
453
|
+
client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "deflate")
|
454
|
+
body = "Hello!"
|
455
|
+
response = client.post("#{dummy.endpoint}/no-content-204", :body => body)
|
456
|
+
|
457
|
+
expect(response.to_s).to eq("")
|
458
|
+
end
|
441
459
|
end
|
442
460
|
|
443
461
|
context "with :normalize_uri" do
|