http 3.1.0 → 5.3.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.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +67 -0
  3. data/.gitignore +6 -9
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/metrics.yml +4 -0
  7. data/.rubocop/rspec.yml +9 -0
  8. data/.rubocop/style.yml +32 -0
  9. data/.rubocop.yml +9 -108
  10. data/.rubocop_todo.yml +219 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +67 -0
  13. data/{CHANGES.md → CHANGES_OLD.md} +358 -0
  14. data/Gemfile +19 -10
  15. data/LICENSE.txt +1 -1
  16. data/README.md +53 -85
  17. data/Rakefile +3 -11
  18. data/SECURITY.md +17 -0
  19. data/http.gemspec +15 -6
  20. data/lib/http/base64.rb +12 -0
  21. data/lib/http/chainable.rb +71 -41
  22. data/lib/http/client.rb +73 -52
  23. data/lib/http/connection.rb +28 -18
  24. data/lib/http/content_type.rb +12 -7
  25. data/lib/http/errors.rb +19 -0
  26. data/lib/http/feature.rb +18 -1
  27. data/lib/http/features/auto_deflate.rb +27 -6
  28. data/lib/http/features/auto_inflate.rb +32 -6
  29. data/lib/http/features/instrumentation.rb +69 -0
  30. data/lib/http/features/logging.rb +53 -0
  31. data/lib/http/features/normalize_uri.rb +17 -0
  32. data/lib/http/features/raise_error.rb +22 -0
  33. data/lib/http/headers/known.rb +3 -0
  34. data/lib/http/headers/normalizer.rb +69 -0
  35. data/lib/http/headers.rb +72 -49
  36. data/lib/http/mime_type/adapter.rb +3 -1
  37. data/lib/http/mime_type/json.rb +1 -0
  38. data/lib/http/options.rb +31 -28
  39. data/lib/http/redirector.rb +56 -4
  40. data/lib/http/request/body.rb +31 -0
  41. data/lib/http/request/writer.rb +29 -9
  42. data/lib/http/request.rb +76 -41
  43. data/lib/http/response/body.rb +6 -4
  44. data/lib/http/response/inflater.rb +1 -1
  45. data/lib/http/response/parser.rb +78 -26
  46. data/lib/http/response/status.rb +4 -3
  47. data/lib/http/response.rb +45 -27
  48. data/lib/http/retriable/client.rb +37 -0
  49. data/lib/http/retriable/delay_calculator.rb +64 -0
  50. data/lib/http/retriable/errors.rb +14 -0
  51. data/lib/http/retriable/performer.rb +153 -0
  52. data/lib/http/timeout/global.rb +29 -47
  53. data/lib/http/timeout/null.rb +12 -8
  54. data/lib/http/timeout/per_operation.rb +32 -57
  55. data/lib/http/uri.rb +75 -1
  56. data/lib/http/version.rb +1 -1
  57. data/lib/http.rb +2 -2
  58. data/spec/lib/http/client_spec.rb +189 -36
  59. data/spec/lib/http/connection_spec.rb +31 -6
  60. data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
  61. data/spec/lib/http/features/instrumentation_spec.rb +81 -0
  62. data/spec/lib/http/features/logging_spec.rb +65 -0
  63. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  64. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  65. data/spec/lib/http/headers_spec.rb +53 -18
  66. data/spec/lib/http/options/headers_spec.rb +6 -2
  67. data/spec/lib/http/options/merge_spec.rb +16 -16
  68. data/spec/lib/http/redirector_spec.rb +147 -3
  69. data/spec/lib/http/request/body_spec.rb +71 -4
  70. data/spec/lib/http/request/writer_spec.rb +45 -2
  71. data/spec/lib/http/request_spec.rb +11 -5
  72. data/spec/lib/http/response/body_spec.rb +5 -5
  73. data/spec/lib/http/response/parser_spec.rb +74 -0
  74. data/spec/lib/http/response/status_spec.rb +3 -3
  75. data/spec/lib/http/response_spec.rb +83 -7
  76. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  77. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  78. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  79. data/spec/lib/http/uri_spec.rb +39 -0
  80. data/spec/lib/http_spec.rb +121 -68
  81. data/spec/regression_specs.rb +7 -0
  82. data/spec/spec_helper.rb +22 -21
  83. data/spec/support/black_hole.rb +1 -1
  84. data/spec/support/dummy_server/servlet.rb +42 -11
  85. data/spec/support/dummy_server.rb +9 -8
  86. data/spec/support/fuubar.rb +21 -0
  87. data/spec/support/http_handling_shared.rb +62 -66
  88. data/spec/support/simplecov.rb +19 -0
  89. data/spec/support/ssl_helper.rb +4 -4
  90. metadata +66 -27
  91. data/.coveralls.yml +0 -1
  92. data/.ruby-version +0 -1
  93. data/.travis.yml +0 -36
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe HTTP::URI::NORMALIZER do
4
+ describe "scheme" do
5
+ it "lower-cases scheme" do
6
+ expect(HTTP::URI::NORMALIZER.call("HttP://example.com").scheme).to eq "http"
7
+ end
8
+ end
9
+
10
+ describe "hostname" do
11
+ it "lower-cases hostname" do
12
+ expect(HTTP::URI::NORMALIZER.call("http://EXAMPLE.com").host).to eq "example.com"
13
+ end
14
+
15
+ it "decodes percent-encoded hostname" do
16
+ expect(HTTP::URI::NORMALIZER.call("http://ex%61mple.com").host).to eq "example.com"
17
+ end
18
+
19
+ it "removes trailing period in hostname" do
20
+ expect(HTTP::URI::NORMALIZER.call("http://example.com.").host).to eq "example.com"
21
+ end
22
+
23
+ it "IDN-encodes non-ASCII hostname" do
24
+ expect(HTTP::URI::NORMALIZER.call("http://exämple.com").host).to eq "xn--exmple-cua.com"
25
+ end
26
+ end
27
+
28
+ describe "path" do
29
+ it "ensures path is not empty" do
30
+ expect(HTTP::URI::NORMALIZER.call("http://example.com").path).to eq "/"
31
+ end
32
+
33
+ it "preserves double slashes in path" do
34
+ expect(HTTP::URI::NORMALIZER.call("http://example.com//a///b").path).to eq "//a///b"
35
+ end
36
+
37
+ it "resolves single-dot segments in path" do
38
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/a/./b").path).to eq "/a/b"
39
+ end
40
+
41
+ it "resolves double-dot segments in path" do
42
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/a/b/../c").path).to eq "/a/c"
43
+ end
44
+
45
+ it "resolves leading double-dot segments in path" do
46
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/../a/b").path).to eq "/a/b"
47
+ end
48
+
49
+ it "percent-encodes control characters in path" do
50
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/\x00\x7F\n").path).to eq "/%00%7F%0A"
51
+ end
52
+
53
+ it "percent-encodes space in path" do
54
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/a b").path).to eq "/a%20b"
55
+ end
56
+
57
+ it "percent-encodes non-ASCII characters in path" do
58
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/キョ").path).to eq "/%E3%82%AD%E3%83%A7"
59
+ end
60
+
61
+ it "does not percent-encode non-special characters in path" do
62
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/~.-_!$&()*,;=:@{}").path).to eq "/~.-_!$&()*,;=:@{}"
63
+ end
64
+
65
+ it "preserves escape sequences in path" do
66
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/%41").path).to eq "/%41"
67
+ end
68
+ end
69
+
70
+ describe "query" do
71
+ it "allows no query" do
72
+ expect(HTTP::URI::NORMALIZER.call("http://example.com").query).to be_nil
73
+ end
74
+
75
+ it "percent-encodes control characters in query" do
76
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/?\x00\x7F\n").query).to eq "%00%7F%0A"
77
+ end
78
+
79
+ it "percent-encodes space in query" do
80
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/?a b").query).to eq "a%20b"
81
+ end
82
+
83
+ it "percent-encodes non-ASCII characters in query" do
84
+ expect(HTTP::URI::NORMALIZER.call("http://example.com?キョ").query).to eq "%E3%82%AD%E3%83%A7"
85
+ end
86
+
87
+ it "does not percent-encode non-special characters in query" do
88
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/?~.-_!$&()*,;=:@{}?").query).to eq "~.-_!$&()*,;=:@{}?"
89
+ end
90
+
91
+ it "preserves escape sequences in query" do
92
+ expect(HTTP::URI::NORMALIZER.call("http://example.com/?%41").query).to eq "%41"
93
+ end
94
+ end
95
+ end
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe HTTP::URI do
4
+ let(:example_ipv6_address) { "2606:2800:220:1:248:1893:25c8:1946" }
5
+
4
6
  let(:example_http_uri_string) { "http://example.com" }
5
7
  let(:example_https_uri_string) { "https://example.com" }
8
+ let(:example_ipv6_uri_string) { "https://[#{example_ipv6_address}]" }
6
9
 
7
10
  subject(:http_uri) { described_class.parse(example_http_uri_string) }
8
11
  subject(:https_uri) { described_class.parse(example_https_uri_string) }
12
+ subject(:ipv6_uri) { described_class.parse(example_ipv6_uri_string) }
9
13
 
10
14
  it "knows URI schemes" do
11
15
  expect(http_uri.scheme).to eq "http"
@@ -20,6 +24,41 @@ RSpec.describe HTTP::URI do
20
24
  expect(https_uri.port).to eq 443
21
25
  end
22
26
 
27
+ describe "#host" do
28
+ it "strips brackets from IPv6 addresses" do
29
+ expect(ipv6_uri.host).to eq("2606:2800:220:1:248:1893:25c8:1946")
30
+ end
31
+ end
32
+
33
+ describe "#normalized_host" do
34
+ it "strips brackets from IPv6 addresses" do
35
+ expect(ipv6_uri.normalized_host).to eq("2606:2800:220:1:248:1893:25c8:1946")
36
+ end
37
+ end
38
+
39
+ describe "#host=" do
40
+ it "updates cached values for #host and #normalized_host" do
41
+ expect(http_uri.host).to eq("example.com")
42
+ expect(http_uri.normalized_host).to eq("example.com")
43
+
44
+ http_uri.host = "[#{example_ipv6_address}]"
45
+
46
+ expect(http_uri.host).to eq(example_ipv6_address)
47
+ expect(http_uri.normalized_host).to eq(example_ipv6_address)
48
+ end
49
+
50
+ it "ensures IPv6 addresses are bracketed in the inner Addressable::URI" do
51
+ expect(http_uri.host).to eq("example.com")
52
+ expect(http_uri.normalized_host).to eq("example.com")
53
+
54
+ http_uri.host = example_ipv6_address
55
+
56
+ expect(http_uri.host).to eq(example_ipv6_address)
57
+ expect(http_uri.normalized_host).to eq(example_ipv6_address)
58
+ expect(http_uri.instance_variable_get(:@uri).host).to eq("[#{example_ipv6_address}]")
59
+ end
60
+ end
61
+
23
62
  describe "#dup" do
24
63
  it "doesn't share internal value between duplicates" do
25
64
  duplicated_uri = http_uri.dup
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
3
3
 
4
4
  require "json"
5
5
 
@@ -55,38 +55,17 @@ RSpec.describe HTTP do
55
55
  end
56
56
 
57
57
  context "with a large request body" do
58
- %w[global null per_operation].each do |timeout|
59
- context "with a #{timeout} timeout" do
60
- [16_000, 16_500, 17_000, 34_000, 68_000].each do |size|
61
- [0, rand(0..100), rand(100..1000)].each do |fuzzer|
62
- context "with a #{size} body and #{fuzzer} of fuzzing" do
63
- let(:client) { HTTP.timeout(timeout, :read => 2, :write => 2, :connect => 2) }
64
-
65
- let(:characters) { ("A".."Z").to_a }
66
- let(:request_body) do
67
- Array.new(size + fuzzer) { |i| characters[i % characters.length] }.join
68
- end
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
@@ -116,7 +95,8 @@ RSpec.describe HTTP do
116
95
  expect(response.to_s).to match(/<!doctype html>/)
117
96
  end
118
97
 
119
- context "ssl" do
98
+ # TODO: htt:s://github.com/httprb/http/issues/627
99
+ xcontext "ssl" do
120
100
  it "responds with the endpoint's body" do
121
101
  response = ssl_client.via(proxy.addr, proxy.port).get dummy_ssl.endpoint
122
102
  expect(response.to_s).to match(/<!doctype html>/)
@@ -152,7 +132,8 @@ RSpec.describe HTTP do
152
132
  expect(response.status).to eq(407)
153
133
  end
154
134
 
155
- context "ssl" do
135
+ # TODO: htt:s://github.com/httprb/http/issues/627
136
+ xcontext "ssl" do
156
137
  it "responds with the endpoint's body" do
157
138
  response = ssl_client.via(proxy.addr, proxy.port, "username", "password").get dummy_ssl.endpoint
158
139
  expect(response.to_s).to match(/<!doctype html>/)
@@ -171,6 +152,35 @@ RSpec.describe HTTP do
171
152
  end
172
153
  end
173
154
 
155
+ describe ".retry" do
156
+ it "ensure endpoint counts retries" do
157
+ expect(HTTP.get("#{dummy.endpoint}/retry-2").to_s).to eq "retried 1x"
158
+ expect(HTTP.get("#{dummy.endpoint}/retry-2").to_s).to eq "retried 2x"
159
+ end
160
+
161
+ it "retries the request" do
162
+ response = HTTP.retriable(delay: 0, retry_statuses: 500...600).get "#{dummy.endpoint}/retry-2"
163
+ expect(response.to_s).to eq "retried 2x"
164
+ end
165
+
166
+ it "retries the request and gives us access to the failed requests" do
167
+ err = nil
168
+ retry_callback = ->(_, _, res) { expect(res.to_s).to match(/^retried \dx$/) }
169
+ begin
170
+ HTTP.retriable(
171
+ should_retry: ->(*) { true },
172
+ tries: 3,
173
+ delay: 0,
174
+ on_retry: retry_callback
175
+ ).get "#{dummy.endpoint}/retry-2"
176
+ rescue HTTP::Error => e
177
+ err = e
178
+ end
179
+
180
+ expect(err.response.to_s).to eq "retried 3x"
181
+ end
182
+ end
183
+
174
184
  context "posting forms to resources" do
175
185
  it "is easy" do
176
186
  response = HTTP.post "#{dummy.endpoint}/form", :form => {:example => "testing-form"}
@@ -194,7 +204,7 @@ RSpec.describe HTTP do
194
204
 
195
205
  context "with encoding option" do
196
206
  it "respects option" do
197
- response = HTTP.get "#{dummy.endpoint}/iso-8859-1", "encoding" => Encoding::BINARY
207
+ response = HTTP.get "#{dummy.endpoint}/iso-8859-1", :encoding => Encoding::BINARY
198
208
  expect(response.to_s.encoding).to eq(Encoding::BINARY)
199
209
  end
200
210
  end
@@ -202,7 +212,7 @@ RSpec.describe HTTP do
202
212
 
203
213
  context "passing a string encoding type" do
204
214
  it "finds encoding" do
205
- response = HTTP.get dummy.endpoint, "encoding" => "ascii"
215
+ response = HTTP.get dummy.endpoint, :encoding => "ascii"
206
216
  expect(response.to_s.encoding).to eq(Encoding::ASCII)
207
217
  end
208
218
  end
@@ -255,15 +265,15 @@ RSpec.describe HTTP do
255
265
 
256
266
  describe ".basic_auth" do
257
267
  it "fails when options is not a Hash" do
258
- expect { HTTP.basic_auth "[FOOBAR]" }.to raise_error
268
+ expect { HTTP.basic_auth "[FOOBAR]" }.to raise_error(NoMethodError)
259
269
  end
260
270
 
261
271
  it "fails when :pass is not given" do
262
- expect { HTTP.basic_auth :user => "[USER]" }.to raise_error
272
+ expect { HTTP.basic_auth :user => "[USER]" }.to raise_error(KeyError)
263
273
  end
264
274
 
265
275
  it "fails when :user is not given" do
266
- expect { HTTP.basic_auth :pass => "[PASS]" }.to raise_error
276
+ expect { HTTP.basic_auth :pass => "[PASS]" }.to raise_error(KeyError)
267
277
  end
268
278
 
269
279
  it "sets Authorization header with proper BasicAuth value" do
@@ -305,22 +315,8 @@ RSpec.describe HTTP do
305
315
  end
306
316
 
307
317
  describe ".timeout" do
308
- context "without timeout type" do
309
- subject(:client) { HTTP.timeout :read => 123 }
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 }
318
+ context "specifying a null timeout" do
319
+ subject(:client) { HTTP.timeout :null }
324
320
 
325
321
  it "sets timeout_class to Null" do
326
322
  expect(client.default_options.timeout_class).
@@ -328,8 +324,8 @@ RSpec.describe HTTP do
328
324
  end
329
325
  end
330
326
 
331
- context "with :per_operation type" do
332
- subject(:client) { HTTP.timeout :per_operation, :read => 123 }
327
+ context "specifying per operation timeouts" do
328
+ subject(:client) { HTTP.timeout :read => 123 }
333
329
 
334
330
  it "sets timeout_class to PerOperation" do
335
331
  expect(client.default_options.timeout_class).
@@ -342,24 +338,28 @@ RSpec.describe HTTP do
342
338
  end
343
339
  end
344
340
 
345
- context "with :global type" do
346
- subject(:client) { HTTP.timeout :global, :read => 123 }
341
+ context "specifying per operation timeouts as frozen hash" do
342
+ let(:frozen_options) { {:read => 123}.freeze }
343
+ subject(:client) { HTTP.timeout(frozen_options) }
344
+
345
+ it "does not raise an error" do
346
+ expect { client }.not_to raise_error
347
+ end
348
+ end
349
+
350
+ context "specifying a global timeout" do
351
+ subject(:client) { HTTP.timeout 123 }
347
352
 
348
353
  it "sets timeout_class to Global" do
349
354
  expect(client.default_options.timeout_class).
350
355
  to be HTTP::Timeout::Global
351
356
  end
352
357
 
353
- it "sets given timeout options" do
358
+ it "sets given timeout option" do
354
359
  expect(client.default_options.timeout_options).
355
- to eq :read_timeout => 123
360
+ to eq :global_timeout => 123
356
361
  end
357
362
  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
363
  end
364
364
 
365
365
  describe ".cookies" do
@@ -469,6 +469,59 @@ RSpec.describe HTTP do
469
469
 
470
470
  expect(response.to_s).to eq("#{body}-deflated")
471
471
  end
472
+
473
+ it "returns empty body for no content response where Content-Encoding is gzip" do
474
+ client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "gzip")
475
+ body = "Hello!"
476
+ response = client.post("#{dummy.endpoint}/no-content-204", :body => body)
477
+
478
+ expect(response.to_s).to eq("")
479
+ end
480
+
481
+ it "returns empty body for no content response where Content-Encoding is deflate" do
482
+ client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "deflate")
483
+ body = "Hello!"
484
+ response = client.post("#{dummy.endpoint}/no-content-204", :body => body)
485
+
486
+ expect(response.to_s).to eq("")
487
+ end
488
+ end
489
+
490
+ context "with :normalize_uri" do
491
+ it "normalizes URI" do
492
+ response = HTTP.get "#{dummy.endpoint}/héllö-wörld"
493
+ expect(response.to_s).to eq("hello world")
494
+ end
495
+
496
+ it "uses the custom URI Normalizer method" do
497
+ client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc})
498
+ response = client.get("#{dummy.endpoint}/héllö-wörld")
499
+ expect(response.status).to eq(400)
500
+ end
501
+
502
+ it "raises if custom URI Normalizer returns invalid path" do
503
+ client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc})
504
+ expect { client.get("#{dummy.endpoint}/hello\nworld") }.
505
+ to raise_error HTTP::RequestError, 'Invalid request URI: "/hello\nworld"'
506
+ end
507
+
508
+ it "raises if custom URI Normalizer returns invalid host" do
509
+ normalizer = lambda do |uri|
510
+ uri.port = nil
511
+ uri.instance_variable_set(:@host, "example\ncom")
512
+ uri
513
+ end
514
+ client = HTTP.use(:normalize_uri => {:normalizer => normalizer})
515
+ expect { client.get(dummy.endpoint) }.
516
+ to raise_error HTTP::RequestError, 'Invalid host: "example\ncom"'
517
+ end
518
+
519
+ it "uses the default URI normalizer" do
520
+ client = HTTP.use :normalize_uri
521
+ expect(HTTP::URI::NORMALIZER).to receive(:call).and_call_original
522
+ response = client.get("#{dummy.endpoint}/héllö-wörld")
523
+ expect(response.to_s).to eq("hello world")
524
+ end
472
525
  end
473
526
  end
474
527
 
@@ -476,7 +529,7 @@ RSpec.describe HTTP do
476
529
  expect { HTTP.get "http://thishostshouldnotexists.com" }.
477
530
  to raise_error HTTP::ConnectionError
478
531
 
479
- expect { HTTP.get "http://127.0.0.1:000" }.
532
+ expect { HTTP.get "http://127.0.0.1:111" }.
480
533
  to raise_error HTTP::ConnectionError
481
534
  end
482
535
  end
@@ -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
data/spec/spec_helper.rb CHANGED
@@ -1,22 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "simplecov"
4
- require "coveralls"
5
-
6
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
7
- [
8
- SimpleCov::Formatter::HTMLFormatter,
9
- Coveralls::SimpleCov::Formatter
10
- ]
11
- )
12
-
13
- SimpleCov.start do
14
- add_filter "/spec/"
15
- minimum_coverage 80
16
- end
3
+ require_relative "./support/simplecov"
4
+ require_relative "./support/fuubar" unless ENV["CI"]
17
5
 
18
6
  require "http"
19
7
  require "rspec/its"
8
+ require "rspec/memory"
20
9
  require "support/capture_warning"
21
10
  require "support/fakeio"
22
11
 
@@ -40,6 +29,13 @@ RSpec.configure do |config|
40
29
  mocks.verify_partial_doubles = true
41
30
  end
42
31
 
32
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
33
+ # have no way to turn it off -- the option exists only for backwards
34
+ # compatibility in RSpec 3). It causes shared context metadata to be
35
+ # inherited by the metadata hash of host groups and examples, rather than
36
+ # triggering implicit auto-inclusion in groups with matching metadata.
37
+ config.shared_context_metadata_behavior = :apply_to_host_groups
38
+
43
39
  # These two settings work together to allow you to limit a spec run
44
40
  # to individual examples or groups you care about by tagging them with
45
41
  # `:focus` metadata. When nothing is tagged with `:focus`, all examples
@@ -48,17 +44,22 @@ RSpec.configure do |config|
48
44
  config.filter_run_excluding :flaky if defined?(JRUBY_VERSION) && ENV["CI"]
49
45
  config.run_all_when_everything_filtered = true
50
46
 
51
- # Limits the available syntax to the non-monkey patched syntax that is recommended.
52
- # For more details, see:
53
- # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
54
- # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
55
- # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
56
- config.disable_monkey_patching!
57
-
58
47
  # This setting enables warnings. It's recommended, but in some cases may
59
48
  # be too noisy due to issues in dependencies.
60
49
  config.warnings = 0 == ENV["GUARD_RSPEC"].to_i
61
50
 
51
+ # Allows RSpec to persist some state between runs in order to support
52
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
53
+ # you configure your source control system to ignore this file.
54
+ config.example_status_persistence_file_path = "spec/examples.txt"
55
+
56
+ # Limits the available syntax to the non-monkey patched syntax that is
57
+ # recommended. For more details, see:
58
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
59
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
60
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
61
+ config.disable_monkey_patching!
62
+
62
63
  # Many RSpec users commonly either run the entire suite or an individual
63
64
  # file, and it's useful to allow more verbose output when running an
64
65
  # individual spec file.
@@ -2,7 +2,7 @@
2
2
 
3
3
  module BlackHole
4
4
  class << self
5
- def method_missing(*) # rubocop: disable Style/MethodMissing
5
+ def method_missing(*)
6
6
  self
7
7
  end
8
8
 
@@ -1,24 +1,30 @@
1
- # frozen_string_literal: true
2
1
  # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "cgi"
3
5
 
4
6
  class DummyServer < WEBrick::HTTPServer
5
- # rubocop:disable Metrics/ClassLength
6
- class Servlet < WEBrick::HTTPServlet::AbstractServlet
7
+ class Servlet < WEBrick::HTTPServlet::AbstractServlet # rubocop:disable Metrics/ClassLength
7
8
  def self.sockets
8
9
  @sockets ||= []
9
10
  end
10
11
 
11
- def not_found(_req, res)
12
+ def not_found(req, res)
12
13
  res.status = 404
13
- res.body = "Not Found"
14
+ res.body = "#{req.unparsed_uri} not found"
14
15
  end
15
16
 
16
17
  def self.handlers
17
18
  @handlers ||= {}
18
19
  end
19
20
 
21
+ def initialize(server, memo)
22
+ super(server)
23
+ @memo = memo
24
+ end
25
+
20
26
  %w[get post head].each do |method|
21
- class_eval <<-RUBY, __FILE__, __LINE__
27
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
22
28
  def self.#{method}(path, &block)
23
29
  handlers["#{method}:\#{path}"] = block
24
30
  end
@@ -26,7 +32,7 @@ class DummyServer < WEBrick::HTTPServer
26
32
  def do_#{method.upcase}(req, res)
27
33
  handler = self.class.handlers["#{method}:\#{req.path}"]
28
34
  return instance_exec(req, res, &handler) if handler
29
- not_found
35
+ not_found(req, res)
30
36
  end
31
37
  RUBY
32
38
  end
@@ -67,7 +73,7 @@ class DummyServer < WEBrick::HTTPServer
67
73
  end
68
74
 
69
75
  get "/params" do |req, res|
70
- next not_found unless "foo=bar" == req.query_string
76
+ next not_found(req, res) unless "foo=bar" == req.query_string
71
77
 
72
78
  res.status = 200
73
79
  res.body = "Params!"
@@ -76,7 +82,7 @@ class DummyServer < WEBrick::HTTPServer
76
82
  get "/multiple-params" do |req, res|
77
83
  params = CGI.parse req.query_string
78
84
 
79
- next not_found unless {"foo" => ["bar"], "baz" => ["quux"]} == params
85
+ next not_found(req, res) unless {"foo" => ["bar"], "baz" => ["quux"]} == params
80
86
 
81
87
  res.status = 200
82
88
  res.body = "More Params!"
@@ -148,11 +154,16 @@ class DummyServer < WEBrick::HTTPServer
148
154
  res.body = req.body
149
155
  end
150
156
 
157
+ get "/héllö-wörld".b do |_req, res|
158
+ res.status = 200
159
+ res.body = "hello world"
160
+ end
161
+
151
162
  post "/encoded-body" do |req, res|
152
163
  res.status = 200
153
164
 
154
165
  res.body = case req["Accept-Encoding"]
155
- when "gzip" then
166
+ when "gzip"
156
167
  res["Content-Encoding"] = "gzip"
157
168
  StringIO.open do |out|
158
169
  Zlib::GzipWriter.wrap(out) do |gz|
@@ -161,12 +172,32 @@ class DummyServer < WEBrick::HTTPServer
161
172
  out.tap(&:rewind).read
162
173
  end
163
174
  end
164
- when "deflate" then
175
+ when "deflate"
165
176
  res["Content-Encoding"] = "deflate"
166
177
  Zlib::Deflate.deflate("#{req.body}-deflated")
167
178
  else
168
179
  "#{req.body}-raw"
169
180
  end
170
181
  end
182
+
183
+ post "/no-content-204" do |req, res|
184
+ res.status = 204
185
+ res.body = ""
186
+
187
+ case req["Accept-Encoding"]
188
+ when "gzip"
189
+ res["Content-Encoding"] = "gzip"
190
+ when "deflate"
191
+ res["Content-Encoding"] = "deflate"
192
+ end
193
+ end
194
+
195
+ get "/retry-2" do |_req, res|
196
+ @memo[:attempts] ||= 0
197
+ @memo[:attempts] += 1
198
+
199
+ res.body = "retried #{@memo[:attempts]}x"
200
+ res.status = @memo[:attempts] == 2 ? 200 : 500
201
+ end
171
202
  end
172
203
  end
@@ -13,20 +13,21 @@ class DummyServer < WEBrick::HTTPServer
13
13
  include ServerConfig
14
14
 
15
15
  CONFIG = {
16
- :BindAddress => "127.0.0.1",
17
- :Port => 0,
18
- :AccessLog => BlackHole,
19
- :Logger => BlackHole
16
+ :BindAddress => "127.0.0.1",
17
+ :Port => 0,
18
+ :AccessLog => BlackHole,
19
+ :Logger => BlackHole
20
20
  }.freeze
21
21
 
22
22
  SSL_CONFIG = CONFIG.merge(
23
- :SSLEnable => true,
24
- :SSLStartImmediately => true
23
+ :SSLEnable => true,
24
+ :SSLStartImmediately => true
25
25
  ).freeze
26
26
 
27
- def initialize(options = {}) # rubocop:disable Style/OptionHash
27
+ def initialize(options = {})
28
28
  super(options[:ssl] ? SSL_CONFIG : CONFIG)
29
- mount("/", Servlet)
29
+ @memo = {}
30
+ mount("/", Servlet, @memo)
30
31
  end
31
32
 
32
33
  def endpoint