http 3.3.0 → 4.4.1
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 +3 -1
- data/.travis.yml +10 -7
- data/CHANGES.md +135 -0
- data/README.md +14 -10
- data/Rakefile +1 -1
- data/http.gemspec +12 -5
- data/lib/http.rb +1 -2
- data/lib/http/chainable.rb +20 -29
- data/lib/http/client.rb +25 -19
- data/lib/http/connection.rb +5 -9
- data/lib/http/feature.rb +14 -0
- data/lib/http/features/auto_deflate.rb +27 -6
- data/lib/http/features/auto_inflate.rb +33 -6
- data/lib/http/features/instrumentation.rb +64 -0
- data/lib/http/features/logging.rb +55 -0
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/options.rb +27 -21
- data/lib/http/redirector.rb +2 -1
- data/lib/http/request.rb +38 -30
- data/lib/http/request/body.rb +30 -1
- data/lib/http/request/writer.rb +21 -7
- data/lib/http/response.rb +7 -15
- data/lib/http/response/parser.rb +56 -16
- data/lib/http/timeout/global.rb +12 -14
- data/lib/http/timeout/per_operation.rb +5 -7
- data/lib/http/uri.rb +13 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +34 -7
- data/spec/lib/http/features/auto_inflate_spec.rb +38 -22
- data/spec/lib/http/features/instrumentation_spec.rb +56 -0
- data/spec/lib/http/features/logging_spec.rb +67 -0
- data/spec/lib/http/redirector_spec.rb +13 -0
- data/spec/lib/http/request/body_spec.rb +51 -0
- data/spec/lib/http/request/writer_spec.rb +20 -0
- data/spec/lib/http/request_spec.rb +6 -0
- data/spec/lib/http/response/parser_spec.rb +45 -0
- data/spec/lib/http/response_spec.rb +3 -4
- data/spec/lib/http_spec.rb +45 -65
- data/spec/regression_specs.rb +7 -0
- data/spec/support/dummy_server/servlet.rb +5 -0
- data/spec/support/http_handling_shared.rb +60 -64
- metadata +32 -21
- data/.ruby-version +0 -1
@@ -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
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Response::Parser do
|
4
|
+
subject(:parser) { described_class.new }
|
5
|
+
let(:raw_response) do
|
6
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: application/json\r\nMy-Header: val\r\nEmpty-Header: \r\n\r\n{}"
|
7
|
+
end
|
8
|
+
let(:expected_headers) do
|
9
|
+
{
|
10
|
+
"Content-Length" => "2",
|
11
|
+
"Content-Type" => "application/json",
|
12
|
+
"My-Header" => "val",
|
13
|
+
"Empty-Header" => ""
|
14
|
+
}
|
15
|
+
end
|
16
|
+
let(:expected_body) { "{}" }
|
17
|
+
|
18
|
+
before do
|
19
|
+
parts.each { |part| subject.add(part) }
|
20
|
+
end
|
21
|
+
|
22
|
+
context "whole response in one part" do
|
23
|
+
let(:parts) { [raw_response] }
|
24
|
+
|
25
|
+
it "parses headers" do
|
26
|
+
expect(subject.headers.to_h).to eq(expected_headers)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "parses body" do
|
30
|
+
expect(subject.read(expected_body.size)).to eq(expected_body)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "response in many parts" do
|
35
|
+
let(:parts) { raw_response.split(//) }
|
36
|
+
|
37
|
+
it "parses headers" do
|
38
|
+
expect(subject.headers.to_h).to eq(expected_headers)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "parses body" do
|
42
|
+
expect(subject.read(expected_body.size)).to eq(expected_body)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -129,13 +129,12 @@ RSpec.describe HTTP::Response do
|
|
129
129
|
end
|
130
130
|
|
131
131
|
describe "#inspect" do
|
132
|
+
subject { response.inspect }
|
133
|
+
|
132
134
|
let(:headers) { {:content_type => "text/plain"} }
|
133
135
|
let(:body) { double :to_s => "foobar" }
|
134
136
|
|
135
|
-
it
|
136
|
-
expect(response.inspect).
|
137
|
-
to eq '#<HTTP::Response/1.1 200 OK {"Content-Type"=>"text/plain"}>'
|
138
|
-
end
|
137
|
+
it { is_expected.to eq '#<HTTP::Response/1.1 200 OK {"Content-Type"=>"text/plain"}>' }
|
139
138
|
end
|
140
139
|
|
141
140
|
describe "#cookies" do
|
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
|
@@ -470,13 +430,33 @@ RSpec.describe HTTP do
|
|
470
430
|
expect(response.to_s).to eq("#{body}-deflated")
|
471
431
|
end
|
472
432
|
end
|
433
|
+
|
434
|
+
context "with :normalize_uri" do
|
435
|
+
it "normalizes URI" do
|
436
|
+
response = HTTP.get "#{dummy.endpoint}/hello world"
|
437
|
+
expect(response.to_s).to eq("hello world")
|
438
|
+
end
|
439
|
+
|
440
|
+
it "uses the custom URI Normalizer method" do
|
441
|
+
client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc})
|
442
|
+
response = client.get("#{dummy.endpoint}/hello world")
|
443
|
+
expect(response.status).to eq(400)
|
444
|
+
end
|
445
|
+
|
446
|
+
it "uses the default URI normalizer" do
|
447
|
+
client = HTTP.use :normalize_uri
|
448
|
+
expect(HTTP::URI::NORMALIZER).to receive(:call).and_call_original
|
449
|
+
response = client.get("#{dummy.endpoint}/hello world")
|
450
|
+
expect(response.to_s).to eq("hello world")
|
451
|
+
end
|
452
|
+
end
|
473
453
|
end
|
474
454
|
|
475
455
|
it "unifies socket errors into HTTP::ConnectionError" do
|
476
456
|
expect { HTTP.get "http://thishostshouldnotexists.com" }.
|
477
457
|
to raise_error HTTP::ConnectionError
|
478
458
|
|
479
|
-
expect { HTTP.get "http://127.0.0.1:
|
459
|
+
expect { HTTP.get "http://127.0.0.1:111" }.
|
480
460
|
to raise_error HTTP::ConnectionError
|
481
461
|
end
|
482
462
|
end
|
data/spec/regression_specs.rb
CHANGED
@@ -14,4 +14,11 @@ RSpec.describe "Regression testing" do
|
|
14
14
|
expect { HTTP.get(google_uri).to_s }.not_to raise_error
|
15
15
|
end
|
16
16
|
end
|
17
|
+
|
18
|
+
describe "#422" do
|
19
|
+
it "reads body when 200 OK response contains Upgrade header" do
|
20
|
+
res = HTTP.get("https://httpbin.org/response-headers?Upgrade=h2,h2c")
|
21
|
+
expect(res.parse(:json)).to include("Upgrade" => "h2,h2c")
|
22
|
+
end
|
23
|
+
end
|
17
24
|
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
|