http 5.0.0.pre3 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|