http 3.3.0 → 4.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/.gitignore +1 -0
- data/.rubocop.yml +1 -0
- data/.travis.yml +6 -6
- data/CHANGES.md +41 -0
- data/README.md +7 -4
- data/Rakefile +1 -1
- data/lib/http.rb +1 -0
- data/lib/http/chainable.rb +14 -19
- data/lib/http/client.rb +11 -4
- data/lib/http/connection.rb +4 -8
- data/lib/http/feature.rb +13 -0
- data/lib/http/features/auto_deflate.rb +20 -5
- data/lib/http/features/auto_inflate.rb +16 -6
- data/lib/http/features/instrumentation.rb +56 -0
- data/lib/http/features/logging.rb +55 -0
- data/lib/http/options.rb +26 -20
- data/lib/http/request.rb +7 -14
- data/lib/http/request/body.rb +5 -0
- data/lib/http/request/writer.rb +21 -7
- data/lib/http/response.rb +7 -15
- data/lib/http/timeout/global.rb +12 -14
- data/lib/http/timeout/per_operation.rb +5 -7
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/features/auto_inflate_spec.rb +17 -21
- data/spec/lib/http/features/instrumentation_spec.rb +56 -0
- data/spec/lib/http/features/logging_spec.rb +74 -0
- data/spec/lib/http/request/body_spec.rb +29 -0
- data/spec/lib/http/request/writer_spec.rb +20 -0
- data/spec/lib/http_spec.rb +25 -65
- data/spec/support/http_handling_shared.rb +60 -64
- metadata +8 -3
- data/.ruby-version +0 -1
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Features::Instrumentation do
|
4
|
+
subject(:feature) { HTTP::Features::Instrumentation.new(:instrumenter => instrumenter) }
|
5
|
+
let(:instrumenter) { TestInstrumenter.new }
|
6
|
+
|
7
|
+
describe "logging the request" do
|
8
|
+
let(:request) do
|
9
|
+
HTTP::Request.new(
|
10
|
+
:verb => :post,
|
11
|
+
:uri => "https://example.com/",
|
12
|
+
:headers => {:accept => "application/json"},
|
13
|
+
:body => '{"hello": "world!"}'
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should log the request" do
|
18
|
+
feature.wrap_request(request)
|
19
|
+
|
20
|
+
expect(instrumenter.output[:start]).to eq(:request => request)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "logging the response" do
|
25
|
+
let(:response) do
|
26
|
+
HTTP::Response.new(
|
27
|
+
:version => "1.1",
|
28
|
+
:uri => "https://example.com",
|
29
|
+
:status => 200,
|
30
|
+
:headers => {:content_type => "application/json"},
|
31
|
+
:body => '{"success": true}'
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should log the response" do
|
36
|
+
feature.wrap_response(response)
|
37
|
+
|
38
|
+
expect(instrumenter.output[:finish]).to eq(:response => response)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class TestInstrumenter < HTTP::Features::Instrumentation::NullInstrumenter
|
43
|
+
attr_reader :output
|
44
|
+
def initialize
|
45
|
+
@output = {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def start(_name, payload)
|
49
|
+
output[:start] = payload
|
50
|
+
end
|
51
|
+
|
52
|
+
def finish(_name, payload)
|
53
|
+
output[:finish] = payload
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Features::Logging do
|
4
|
+
subject(:feature) { HTTP::Features::Logging.new(:logger => logger) }
|
5
|
+
let(:logger) { TestLogger.new }
|
6
|
+
|
7
|
+
describe "logging the request" do
|
8
|
+
let(:request) do
|
9
|
+
HTTP::Request.new(
|
10
|
+
:verb => :post,
|
11
|
+
:uri => "https://example.com/",
|
12
|
+
:headers => {:accept => "application/json"},
|
13
|
+
:body => '{"hello": "world!"}'
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should log the request" do
|
18
|
+
feature.wrap_request(request)
|
19
|
+
|
20
|
+
expect(logger.output).to eq(
|
21
|
+
[
|
22
|
+
"> POST https://example.com/",
|
23
|
+
<<~REQ.strip
|
24
|
+
Accept: application/json
|
25
|
+
Host: example.com
|
26
|
+
User-Agent: http.rb/4.0.0.dev
|
27
|
+
|
28
|
+
{"hello": "world!"}
|
29
|
+
REQ
|
30
|
+
]
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "logging the response" do
|
36
|
+
let(:response) do
|
37
|
+
HTTP::Response.new(
|
38
|
+
:version => "1.1",
|
39
|
+
:uri => "https://example.com",
|
40
|
+
:status => 200,
|
41
|
+
:headers => {:content_type => "application/json"},
|
42
|
+
:body => '{"success": true}'
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should log the response" do
|
47
|
+
feature.wrap_response(response)
|
48
|
+
|
49
|
+
expect(logger.output).to eq(
|
50
|
+
[
|
51
|
+
"< 200 OK",
|
52
|
+
<<~REQ.strip
|
53
|
+
Content-Type: application/json
|
54
|
+
|
55
|
+
{"success": true}
|
56
|
+
REQ
|
57
|
+
]
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class TestLogger
|
63
|
+
attr_reader :output
|
64
|
+
def initialize
|
65
|
+
@output = []
|
66
|
+
end
|
67
|
+
|
68
|
+
%w[fatal error warn info debug].each do |level|
|
69
|
+
define_method(level.to_sym) do |*args, &block|
|
70
|
+
@output << (block ? block.call : args)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -157,4 +157,33 @@ RSpec.describe HTTP::Request::Body do
|
|
157
157
|
end
|
158
158
|
end
|
159
159
|
end
|
160
|
+
|
161
|
+
describe "#==" do
|
162
|
+
context "when sources are equivalent" do
|
163
|
+
let(:body1) { HTTP::Request::Body.new("content") }
|
164
|
+
let(:body2) { HTTP::Request::Body.new("content") }
|
165
|
+
|
166
|
+
it "returns true" do
|
167
|
+
expect(body1).to eq body2
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context "when sources are not equivalent" do
|
172
|
+
let(:body1) { HTTP::Request::Body.new("content") }
|
173
|
+
let(:body2) { HTTP::Request::Body.new(nil) }
|
174
|
+
|
175
|
+
it "returns false" do
|
176
|
+
expect(body1).not_to eq body2
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context "when objects are not of the same class" do
|
181
|
+
let(:body1) { HTTP::Request::Body.new("content") }
|
182
|
+
let(:body2) { "content" }
|
183
|
+
|
184
|
+
it "returns false" do
|
185
|
+
expect(body1).not_to eq body2
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
160
189
|
end
|
@@ -74,5 +74,25 @@ RSpec.describe HTTP::Request::Writer do
|
|
74
74
|
].join
|
75
75
|
end
|
76
76
|
end
|
77
|
+
|
78
|
+
context "when server won't accept any more data" do
|
79
|
+
before do
|
80
|
+
expect(io).to receive(:write).and_raise(Errno::EPIPE)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "aborts silently" do
|
84
|
+
writer.stream
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context "when writing to socket raises an exception" do
|
89
|
+
before do
|
90
|
+
expect(io).to receive(:write).and_raise(Errno::ECONNRESET)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "raises a ConnectionError" do
|
94
|
+
expect { writer.stream }.to raise_error(HTTP::ConnectionError)
|
95
|
+
end
|
96
|
+
end
|
77
97
|
end
|
78
98
|
end
|
data/spec/lib/http_spec.rb
CHANGED
@@ -55,38 +55,17 @@ RSpec.describe HTTP do
|
|
55
55
|
end
|
56
56
|
|
57
57
|
context "with a large request body" do
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
it "returns a large body" do
|
71
|
-
response = client.post("#{dummy.endpoint}/echo-body", :body => request_body)
|
72
|
-
|
73
|
-
expect(response.body.to_s).to eq(request_body)
|
74
|
-
expect(response.headers["Content-Length"].to_i).to eq(request_body.bytesize)
|
75
|
-
end
|
76
|
-
|
77
|
-
context "when bytesize != length" do
|
78
|
-
let(:characters) { ("A".."Z").to_a.push("“") }
|
79
|
-
|
80
|
-
it "returns a large body" do
|
81
|
-
body = {:data => request_body}
|
82
|
-
response = client.post("#{dummy.endpoint}/echo-body", :json => body)
|
83
|
-
|
84
|
-
expect(CGI.unescape(response.body.to_s)).to eq(body.to_json)
|
85
|
-
expect(response.headers["Content-Length"].to_i).to eq(body.to_json.bytesize)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
58
|
+
let(:request_body) { "“" * 1_000_000 } # use multi-byte character
|
59
|
+
|
60
|
+
[:null, 6, {:read => 2, :write => 2, :connect => 2}].each do |timeout|
|
61
|
+
context "with `.timeout(#{timeout.inspect})`" do
|
62
|
+
let(:client) { HTTP.timeout(timeout) }
|
63
|
+
|
64
|
+
it "writes the whole body" do
|
65
|
+
response = client.post "#{dummy.endpoint}/echo-body", :body => request_body
|
66
|
+
|
67
|
+
expect(response.body.to_s).to eq(request_body.b)
|
68
|
+
expect(response.headers["Content-Length"].to_i).to eq request_body.bytesize
|
90
69
|
end
|
91
70
|
end
|
92
71
|
end
|
@@ -194,7 +173,7 @@ RSpec.describe HTTP do
|
|
194
173
|
|
195
174
|
context "with encoding option" do
|
196
175
|
it "respects option" do
|
197
|
-
response = HTTP.get "#{dummy.endpoint}/iso-8859-1",
|
176
|
+
response = HTTP.get "#{dummy.endpoint}/iso-8859-1", :encoding => Encoding::BINARY
|
198
177
|
expect(response.to_s.encoding).to eq(Encoding::BINARY)
|
199
178
|
end
|
200
179
|
end
|
@@ -202,7 +181,7 @@ RSpec.describe HTTP do
|
|
202
181
|
|
203
182
|
context "passing a string encoding type" do
|
204
183
|
it "finds encoding" do
|
205
|
-
response = HTTP.get dummy.endpoint,
|
184
|
+
response = HTTP.get dummy.endpoint, :encoding => "ascii"
|
206
185
|
expect(response.to_s.encoding).to eq(Encoding::ASCII)
|
207
186
|
end
|
208
187
|
end
|
@@ -255,15 +234,15 @@ RSpec.describe HTTP do
|
|
255
234
|
|
256
235
|
describe ".basic_auth" do
|
257
236
|
it "fails when options is not a Hash" do
|
258
|
-
expect { HTTP.basic_auth "[FOOBAR]" }.to raise_error
|
237
|
+
expect { HTTP.basic_auth "[FOOBAR]" }.to raise_error(NoMethodError)
|
259
238
|
end
|
260
239
|
|
261
240
|
it "fails when :pass is not given" do
|
262
|
-
expect { HTTP.basic_auth :user => "[USER]" }.to raise_error
|
241
|
+
expect { HTTP.basic_auth :user => "[USER]" }.to raise_error(KeyError)
|
263
242
|
end
|
264
243
|
|
265
244
|
it "fails when :user is not given" do
|
266
|
-
expect { HTTP.basic_auth :pass => "[PASS]" }.to raise_error
|
245
|
+
expect { HTTP.basic_auth :pass => "[PASS]" }.to raise_error(KeyError)
|
267
246
|
end
|
268
247
|
|
269
248
|
it "sets Authorization header with proper BasicAuth value" do
|
@@ -305,22 +284,8 @@ RSpec.describe HTTP do
|
|
305
284
|
end
|
306
285
|
|
307
286
|
describe ".timeout" do
|
308
|
-
context "
|
309
|
-
subject(:client) { HTTP.timeout :
|
310
|
-
|
311
|
-
it "sets timeout_class to PerOperation" do
|
312
|
-
expect(client.default_options.timeout_class).
|
313
|
-
to be HTTP::Timeout::PerOperation
|
314
|
-
end
|
315
|
-
|
316
|
-
it "sets given timeout options" do
|
317
|
-
expect(client.default_options.timeout_options).
|
318
|
-
to eq :read_timeout => 123
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
|
-
context "with :null type" do
|
323
|
-
subject(:client) { HTTP.timeout :null, :read => 123 }
|
287
|
+
context "specifying a null timeout" do
|
288
|
+
subject(:client) { HTTP.timeout :null }
|
324
289
|
|
325
290
|
it "sets timeout_class to Null" do
|
326
291
|
expect(client.default_options.timeout_class).
|
@@ -328,8 +293,8 @@ RSpec.describe HTTP do
|
|
328
293
|
end
|
329
294
|
end
|
330
295
|
|
331
|
-
context "
|
332
|
-
subject(:client) { HTTP.timeout :
|
296
|
+
context "specifying per operation timeouts" do
|
297
|
+
subject(:client) { HTTP.timeout :read => 123 }
|
333
298
|
|
334
299
|
it "sets timeout_class to PerOperation" do
|
335
300
|
expect(client.default_options.timeout_class).
|
@@ -342,24 +307,19 @@ RSpec.describe HTTP do
|
|
342
307
|
end
|
343
308
|
end
|
344
309
|
|
345
|
-
context "
|
346
|
-
subject(:client) { HTTP.timeout
|
310
|
+
context "specifying a global timeout" do
|
311
|
+
subject(:client) { HTTP.timeout 123 }
|
347
312
|
|
348
313
|
it "sets timeout_class to Global" do
|
349
314
|
expect(client.default_options.timeout_class).
|
350
315
|
to be HTTP::Timeout::Global
|
351
316
|
end
|
352
317
|
|
353
|
-
it "sets given timeout
|
318
|
+
it "sets given timeout option" do
|
354
319
|
expect(client.default_options.timeout_options).
|
355
|
-
to eq :
|
320
|
+
to eq :global_timeout => 123
|
356
321
|
end
|
357
322
|
end
|
358
|
-
|
359
|
-
it "fails with unknown timeout type" do
|
360
|
-
expect { HTTP.timeout(:foobar, :read => 123) }.
|
361
|
-
to raise_error(ArgumentError, /foobar/)
|
362
|
-
end
|
363
323
|
end
|
364
324
|
|
365
325
|
describe ".cookies" do
|
@@ -476,7 +436,7 @@ RSpec.describe HTTP do
|
|
476
436
|
expect { HTTP.get "http://thishostshouldnotexists.com" }.
|
477
437
|
to raise_error HTTP::ConnectionError
|
478
438
|
|
479
|
-
expect { HTTP.get "http://127.0.0.1:
|
439
|
+
expect { HTTP.get "http://127.0.0.1:111" }.
|
480
440
|
to raise_error HTTP::ConnectionError
|
481
441
|
end
|
482
442
|
end
|
@@ -1,14 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
RSpec.shared_context "HTTP handling" do
|
4
|
-
|
5
|
-
let(:
|
6
|
-
|
7
|
-
|
4
|
+
context "without timeouts" do
|
5
|
+
let(:options) { {:timeout_class => HTTP::Timeout::Null, :timeout_options => {}} }
|
6
|
+
|
7
|
+
it "works" do
|
8
|
+
expect(client.get(server.endpoint).body.to_s).to eq("<!doctype html>")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
context "with a per operation timeout" do
|
13
|
+
let(:response) { client.get(server.endpoint).body.to_s }
|
8
14
|
|
9
15
|
let(:options) do
|
10
16
|
{
|
11
|
-
:timeout_class =>
|
17
|
+
:timeout_class => HTTP::Timeout::PerOperation,
|
12
18
|
:timeout_options => {
|
13
19
|
:connect_timeout => conn_timeout,
|
14
20
|
:read_timeout => read_timeout,
|
@@ -16,87 +22,77 @@ RSpec.shared_context "HTTP handling" do
|
|
16
22
|
}
|
17
23
|
}
|
18
24
|
end
|
25
|
+
let(:conn_timeout) { 1 }
|
26
|
+
let(:read_timeout) { 1 }
|
27
|
+
let(:write_timeout) { 1 }
|
19
28
|
|
20
|
-
|
21
|
-
|
22
|
-
let(:conn_timeout) { 0 }
|
23
|
-
let(:read_timeout) { 0 }
|
24
|
-
let(:write_timeout) { 0 }
|
25
|
-
|
26
|
-
it "works" do
|
27
|
-
expect(client.get(server.endpoint).body.to_s).to eq("<!doctype html>")
|
28
|
-
end
|
29
|
+
it "works" do
|
30
|
+
expect(response).to eq("<!doctype html>")
|
29
31
|
end
|
30
32
|
|
31
|
-
context "
|
32
|
-
|
33
|
-
|
34
|
-
let(:response) { client.get(server.endpoint).body.to_s }
|
35
|
-
|
36
|
-
it "works" do
|
37
|
-
expect(response).to eq("<!doctype html>")
|
38
|
-
end
|
33
|
+
context "connection" do
|
34
|
+
context "of 1" do
|
35
|
+
let(:conn_timeout) { 1 }
|
39
36
|
|
40
|
-
|
41
|
-
|
42
|
-
let(:conn_timeout) { 1 }
|
43
|
-
|
44
|
-
it "does not time out" do
|
45
|
-
expect { response }.to_not raise_error
|
46
|
-
end
|
37
|
+
it "does not time out" do
|
38
|
+
expect { response }.to_not raise_error
|
47
39
|
end
|
48
40
|
end
|
41
|
+
end
|
49
42
|
|
50
|
-
|
51
|
-
|
52
|
-
|
43
|
+
context "read" do
|
44
|
+
context "of 0" do
|
45
|
+
let(:read_timeout) { 0 }
|
53
46
|
|
54
|
-
|
55
|
-
|
56
|
-
end
|
47
|
+
it "times out", :flaky do
|
48
|
+
expect { response }.to raise_error(HTTP::TimeoutError, /Read/i)
|
57
49
|
end
|
50
|
+
end
|
58
51
|
|
59
|
-
|
60
|
-
|
52
|
+
context "of 2.5" do
|
53
|
+
let(:read_timeout) { 2.5 }
|
61
54
|
|
62
|
-
|
63
|
-
|
64
|
-
end
|
55
|
+
it "does not time out", :flaky do
|
56
|
+
expect { client.get("#{server.endpoint}/sleep").body.to_s }.to_not raise_error
|
65
57
|
end
|
66
58
|
end
|
67
59
|
end
|
60
|
+
end
|
68
61
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
62
|
+
context "with a global timeout" do
|
63
|
+
let(:options) do
|
64
|
+
{
|
65
|
+
:timeout_class => HTTP::Timeout::Global,
|
66
|
+
:timeout_options => {
|
67
|
+
:global_timeout => global_timeout
|
68
|
+
}
|
69
|
+
}
|
70
|
+
end
|
71
|
+
let(:global_timeout) { 1 }
|
77
72
|
|
78
|
-
|
79
|
-
expect(TCPSocket).to receive(:open) do
|
80
|
-
sleep 1.25
|
81
|
-
end
|
73
|
+
let(:response) { client.get(server.endpoint).body.to_s }
|
82
74
|
|
83
|
-
|
75
|
+
it "errors if connecting takes too long" do
|
76
|
+
expect(TCPSocket).to receive(:open) do
|
77
|
+
sleep 1.25
|
84
78
|
end
|
85
79
|
|
86
|
-
|
87
|
-
|
88
|
-
to raise_error(HTTP::TimeoutError, /Timed out/)
|
89
|
-
end
|
80
|
+
expect { response }.to raise_error(HTTP::TimeoutError, /execution/)
|
81
|
+
end
|
90
82
|
|
91
|
-
|
92
|
-
|
83
|
+
it "errors if reading takes too long" do
|
84
|
+
expect { client.get("#{server.endpoint}/sleep").body.to_s }.
|
85
|
+
to raise_error(HTTP::TimeoutError, /Timed out/)
|
86
|
+
end
|
93
87
|
|
94
|
-
|
88
|
+
context "it resets state when reusing connections" do
|
89
|
+
let(:extra_options) { {:persistent => server.endpoint} }
|
95
90
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
91
|
+
let(:global_timeout) { 2.5 }
|
92
|
+
|
93
|
+
it "does not timeout", :flaky do
|
94
|
+
client.get("#{server.endpoint}/sleep").body.to_s
|
95
|
+
client.get("#{server.endpoint}/sleep").body.to_s
|
100
96
|
end
|
101
97
|
end
|
102
98
|
end
|