http 5.1.1 → 5.3.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -24
  3. data/.rubocop/metrics.yml +4 -0
  4. data/.rubocop/rspec.yml +9 -0
  5. data/.rubocop.yml +1 -0
  6. data/.rubocop_todo.yml +45 -32
  7. data/CHANGELOG.md +57 -0
  8. data/{CHANGES.md → CHANGES_OLD.md} +1 -1
  9. data/Gemfile +1 -0
  10. data/README.md +4 -2
  11. data/SECURITY.md +13 -1
  12. data/http.gemspec +8 -2
  13. data/lib/http/base64.rb +12 -0
  14. data/lib/http/chainable.rb +27 -3
  15. data/lib/http/client.rb +1 -1
  16. data/lib/http/connection.rb +12 -3
  17. data/lib/http/errors.rb +16 -0
  18. data/lib/http/feature.rb +2 -1
  19. data/lib/http/features/instrumentation.rb +6 -1
  20. data/lib/http/features/raise_error.rb +22 -0
  21. data/lib/http/headers/normalizer.rb +69 -0
  22. data/lib/http/headers.rb +26 -40
  23. data/lib/http/request/writer.rb +2 -1
  24. data/lib/http/request.rb +15 -5
  25. data/lib/http/retriable/client.rb +37 -0
  26. data/lib/http/retriable/delay_calculator.rb +64 -0
  27. data/lib/http/retriable/errors.rb +14 -0
  28. data/lib/http/retriable/performer.rb +153 -0
  29. data/lib/http/timeout/null.rb +8 -5
  30. data/lib/http/uri.rb +18 -2
  31. data/lib/http/version.rb +1 -1
  32. data/lib/http.rb +1 -0
  33. data/spec/lib/http/client_spec.rb +1 -0
  34. data/spec/lib/http/connection_spec.rb +23 -1
  35. data/spec/lib/http/features/instrumentation_spec.rb +19 -0
  36. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  37. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  38. data/spec/lib/http/options/headers_spec.rb +5 -1
  39. data/spec/lib/http/redirector_spec.rb +6 -5
  40. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  41. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  42. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  43. data/spec/lib/http_spec.rb +49 -3
  44. data/spec/spec_helper.rb +1 -0
  45. data/spec/support/dummy_server/servlet.rb +19 -6
  46. data/spec/support/dummy_server.rb +2 -1
  47. metadata +28 -8
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe HTTP::Headers::Normalizer do
4
+ subject(:normalizer) { described_class.new }
5
+
6
+ include_context RSpec::Memory
7
+
8
+ describe "#call" do
9
+ it "normalizes the header" do
10
+ expect(normalizer.call("content_type")).to eq "Content-Type"
11
+ end
12
+
13
+ it "returns a non-frozen string" do
14
+ expect(normalizer.call("content_type")).not_to be_frozen
15
+ end
16
+
17
+ it "evicts the oldest item when cache is full" do
18
+ max_headers = (1..described_class::Cache::MAX_SIZE).map { |i| "Header#{i}" }
19
+ max_headers.each { |header| normalizer.call(header) }
20
+ normalizer.call("New-Header")
21
+ cache_store = normalizer.instance_variable_get(:@cache).instance_variable_get(:@store)
22
+ expect(cache_store.keys).to eq(max_headers[1..] + ["New-Header"])
23
+ end
24
+
25
+ it "retuns mutable strings" do
26
+ normalized_headers = Array.new(3) { normalizer.call("content_type") }
27
+
28
+ expect(normalized_headers)
29
+ .to satisfy { |arr| arr.uniq.size == 1 }
30
+ .and(satisfy { |arr| arr.map(&:object_id).uniq.size == normalized_headers.size })
31
+ .and(satisfy { |arr| arr.none?(&:frozen?) })
32
+ end
33
+
34
+ it "allocates minimal memory for normalization of the same header" do
35
+ normalizer.call("accept") # XXX: Ensure normalizer is pre-allocated
36
+
37
+ # On first call it is expected to allocate during normalization
38
+ expect { normalizer.call("content_type") }.to limit_allocations(
39
+ Array => 1,
40
+ MatchData => 1,
41
+ String => 6
42
+ )
43
+
44
+ # On subsequent call it is expected to only allocate copy of a cached string
45
+ expect { normalizer.call("content_type") }.to limit_allocations(
46
+ Array => 0,
47
+ MatchData => 0,
48
+ String => 1
49
+ )
50
+ end
51
+ end
52
+ end
@@ -14,7 +14,11 @@ RSpec.describe HTTP::Options, "headers" do
14
14
  end
15
15
 
16
16
  it "accepts any object that respond to :to_hash" do
17
- x = Struct.new(:to_hash).new("accept" => "json")
17
+ x = if RUBY_VERSION >= "3.2.0"
18
+ Data.define(:to_hash).new(:to_hash => { "accept" => "json" })
19
+ else
20
+ Struct.new(:to_hash).new({ "accept" => "json" })
21
+ end
18
22
  expect(opts.with_headers(x).headers["accept"]).to eq("json")
19
23
  end
20
24
  end
@@ -115,12 +115,13 @@ RSpec.describe HTTP::Redirector do
115
115
  expect(req_cookie).to eq request_cookies.shift
116
116
  hops.shift
117
117
  end
118
+
118
119
  expect(res.to_s).to eq "bar"
119
- cookies = res.cookies.cookies.to_h { |c| [c.name, c.value] }
120
- expect(cookies["foo"]).to eq "42"
121
- expect(cookies["bar"]).to eq "53"
122
- expect(cookies["baz"]).to eq "65"
123
- expect(cookies["deleted"]).to eq nil
120
+ expect(res.cookies.cookies.to_h { |c| [c.name, c.value] }).to eq({
121
+ "foo" => "42",
122
+ "bar" => "53",
123
+ "baz" => "65"
124
+ })
124
125
  end
125
126
 
126
127
  it "returns original cookies in response" do
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe HTTP::Retriable::DelayCalculator do
4
+ let(:response) do
5
+ HTTP::Response.new(
6
+ status: 200,
7
+ version: "1.1",
8
+ headers: {},
9
+ body: "Hello world!",
10
+ request: HTTP::Request.new(verb: :get, uri: "http://example.com")
11
+ )
12
+ end
13
+
14
+ def call_delay(iterations, **options)
15
+ described_class.new(options).call(iterations, response)
16
+ end
17
+
18
+ def call_retry_header(value, **options)
19
+ response.headers["Retry-After"] = value
20
+ described_class.new(options).call(rand(1...100), response)
21
+ end
22
+
23
+ it "prevents negative sleep time" do
24
+ expect(call_delay(20, delay: -20)).to eq 0
25
+ end
26
+
27
+ it "backs off exponentially" do
28
+ expect(call_delay(1)).to be_between 0, 1
29
+ expect(call_delay(2)).to be_between 1, 2
30
+ expect(call_delay(3)).to be_between 3, 4
31
+ expect(call_delay(4)).to be_between 7, 8
32
+ expect(call_delay(5)).to be_between 15, 16
33
+ end
34
+
35
+ it "can have a maximum wait time" do
36
+ expect(call_delay(1, max_delay: 5)).to be_between 0, 1
37
+ expect(call_delay(5, max_delay: 5)).to eq 5
38
+ end
39
+
40
+ it "respects Retry-After headers as integer" do
41
+ delay_time = rand(6...2500)
42
+ header_value = delay_time.to_s
43
+ expect(call_retry_header(header_value)).to eq delay_time
44
+ expect(call_retry_header(header_value, max_delay: 5)).to eq 5
45
+ end
46
+
47
+ it "respects Retry-After headers as rfc2822 timestamp" do
48
+ delay_time = rand(6...2500)
49
+ header_value = (Time.now.gmtime + delay_time).to_datetime.rfc2822.sub("+0000", "GMT")
50
+ expect(call_retry_header(header_value)).to be_within(1).of(delay_time)
51
+ expect(call_retry_header(header_value, max_delay: 5)).to eq 5
52
+ end
53
+
54
+ it "respects Retry-After headers as rfc2822 timestamp in the past" do
55
+ delay_time = rand(6...2500)
56
+ header_value = (Time.now.gmtime - delay_time).to_datetime.rfc2822.sub("+0000", "GMT")
57
+ expect(call_retry_header(header_value)).to eq 0
58
+ end
59
+
60
+ it "does not error on invalid Retry-After header" do
61
+ [ # invalid strings
62
+ "This is a string with a number 5 in it",
63
+ "8 Eight is the first digit in this string",
64
+ "This is a string with a #{Time.now.gmtime.to_datetime.rfc2822} timestamp in it"
65
+ ].each do |header_value|
66
+ expect(call_retry_header(header_value)).to eq 0
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe HTTP::Retriable::Performer do
4
+ let(:client) do
5
+ HTTP::Client.new
6
+ end
7
+
8
+ let(:response) do
9
+ HTTP::Response.new(
10
+ status: 200,
11
+ version: "1.1",
12
+ headers: {},
13
+ body: "Hello world!",
14
+ request: request
15
+ )
16
+ end
17
+
18
+ let(:request) do
19
+ HTTP::Request.new(
20
+ verb: :get,
21
+ uri: "http://example.com"
22
+ )
23
+ end
24
+
25
+ let(:perform_spy) { { counter: 0 } }
26
+ let(:counter_spy) { perform_spy[:counter] }
27
+
28
+ before do
29
+ stub_const("CustomException", Class.new(StandardError))
30
+ end
31
+
32
+ def perform(options = {}, client_arg = client, request_arg = request, &block)
33
+ # by explicitly overwriting the default delay, we make a much faster test suite
34
+ default_options = { delay: 0 }
35
+ options = default_options.merge(options)
36
+
37
+ HTTP::Retriable::Performer
38
+ .new(options)
39
+ .perform(client_arg, request_arg) do
40
+ perform_spy[:counter] += 1
41
+ block ? yield : response
42
+ end
43
+ end
44
+
45
+ def measure_wait
46
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
47
+ result = yield
48
+ t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
+ [t2 - t1, result]
50
+ end
51
+
52
+ describe "#perform" do
53
+ describe "expected exception" do
54
+ it "retries the request" do
55
+ expect do
56
+ perform(exceptions: [CustomException], tries: 2) do
57
+ raise CustomException
58
+ end
59
+ end.to raise_error HTTP::OutOfRetriesError
60
+
61
+ expect(counter_spy).to eq 2
62
+ end
63
+ end
64
+
65
+ describe "unexpected exception" do
66
+ it "does not retry the request" do
67
+ expect do
68
+ perform(exceptions: [], tries: 2) do
69
+ raise CustomException
70
+ end
71
+ end.to raise_error CustomException
72
+
73
+ expect(counter_spy).to eq 1
74
+ end
75
+ end
76
+
77
+ describe "expected status codes" do
78
+ def response(**options)
79
+ HTTP::Response.new(
80
+ {
81
+ status: 200,
82
+ version: "1.1",
83
+ headers: {},
84
+ body: "Hello world!",
85
+ request: request
86
+ }.merge(options)
87
+ )
88
+ end
89
+
90
+ it "retries the request" do
91
+ expect do
92
+ perform(retry_statuses: [200], tries: 2)
93
+ end.to raise_error HTTP::OutOfRetriesError
94
+
95
+ expect(counter_spy).to eq 2
96
+ end
97
+
98
+ describe "status codes can be expressed in many ways" do
99
+ [
100
+ 301,
101
+ [200, 301, 485],
102
+ 250...400,
103
+ [250...Float::INFINITY],
104
+ ->(status_code) { status_code == 301 },
105
+ [->(status_code) { status_code == 301 }]
106
+ ].each do |retry_statuses|
107
+ it retry_statuses.to_s do
108
+ expect do
109
+ perform(retry_statuses: retry_statuses, tries: 2) do
110
+ response(status: 301)
111
+ end
112
+ end.to raise_error HTTP::OutOfRetriesError
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ describe "unexpected status code" do
119
+ it "does not retry the request" do
120
+ expect(
121
+ perform(retry_statuses: [], tries: 2)
122
+ ).to eq response
123
+
124
+ expect(counter_spy).to eq 1
125
+ end
126
+ end
127
+
128
+ describe "on_retry callback" do
129
+ it "calls the on_retry callback on each retry with exception" do
130
+ callback_call_spy = 0
131
+
132
+ callback_spy = proc do |callback_request, error, callback_response|
133
+ expect(callback_request).to eq request
134
+ expect(error).to be_a HTTP::TimeoutError
135
+ expect(callback_response).to be_nil
136
+ callback_call_spy += 1
137
+ end
138
+
139
+ expect do
140
+ perform(tries: 3, on_retry: callback_spy) do
141
+ raise HTTP::TimeoutError
142
+ end
143
+ end.to raise_error HTTP::OutOfRetriesError
144
+
145
+ expect(callback_call_spy).to eq 2
146
+ end
147
+
148
+ it "calls the on_retry callback on each retry with response" do
149
+ callback_call_spy = 0
150
+
151
+ callback_spy = proc do |callback_request, error, callback_response|
152
+ expect(callback_request).to eq request
153
+ expect(error).to be_nil
154
+ expect(callback_response).to be response
155
+ callback_call_spy += 1
156
+ end
157
+
158
+ expect do
159
+ perform(retry_statuses: [200], tries: 3, on_retry: callback_spy)
160
+ end.to raise_error HTTP::OutOfRetriesError
161
+
162
+ expect(callback_call_spy).to eq 2
163
+ end
164
+ end
165
+
166
+ describe "delay option" do
167
+ let(:timing_slack) { 0.05 }
168
+
169
+ it "can be a positive number" do
170
+ time, = measure_wait do
171
+ perform(delay: 0.1, tries: 3, should_retry: ->(*) { true })
172
+ rescue HTTP::OutOfRetriesError
173
+ end
174
+ expect(time).to be_within(timing_slack).of(0.2)
175
+ end
176
+
177
+ it "can be a proc number" do
178
+ time, = measure_wait do
179
+ perform(delay: ->(attempt) { attempt / 10.0 }, tries: 3, should_retry: ->(*) { true })
180
+ rescue HTTP::OutOfRetriesError
181
+ end
182
+ expect(time).to be_within(timing_slack).of(0.3)
183
+ end
184
+
185
+ it "receives correct retry number when a proc" do
186
+ retry_count = 0
187
+ retry_proc = proc do |attempt|
188
+ expect(attempt).to eq(retry_count).and(be > 0)
189
+ 0
190
+ end
191
+ begin
192
+ perform(delay: retry_proc, should_retry: ->(*) { true }) do
193
+ retry_count += 1
194
+ response
195
+ end
196
+ rescue HTTP::OutOfRetriesError
197
+ end
198
+ end
199
+ end
200
+
201
+ describe "should_retry option" do
202
+ it "decides if the request should be retried" do # rubocop:disable RSpec/MultipleExpectations
203
+ retry_proc = proc do |req, err, res, attempt|
204
+ expect(req).to eq request
205
+ if res
206
+ expect(err).to be_nil
207
+ expect(res).to be response
208
+ else
209
+ expect(err).to be_a CustomException
210
+ expect(res).to be_nil
211
+ end
212
+
213
+ attempt < 5
214
+ end
215
+
216
+ begin
217
+ perform(should_retry: retry_proc) do
218
+ rand < 0.5 ? response : raise(CustomException)
219
+ end
220
+ rescue CustomException
221
+ end
222
+
223
+ expect(counter_spy).to eq 5
224
+ end
225
+
226
+ it "raises the original error if not retryable" do
227
+ retry_proc = ->(*) { false }
228
+
229
+ expect do
230
+ perform(should_retry: retry_proc) do
231
+ raise CustomException
232
+ end
233
+ end.to raise_error CustomException
234
+
235
+ expect(counter_spy).to eq 1
236
+ end
237
+
238
+ it "raises HTTP::OutOfRetriesError if retryable" do
239
+ retry_proc = ->(*) { true }
240
+
241
+ expect do
242
+ perform(should_retry: retry_proc) do
243
+ raise CustomException
244
+ end
245
+ end.to raise_error HTTP::OutOfRetriesError
246
+
247
+ expect(counter_spy).to eq 5
248
+ end
249
+ end
250
+ end
251
+
252
+ describe "connection closing" do
253
+ let(:client) { double(:client) }
254
+
255
+ it "does not close the connection if we get a propper response" do
256
+ expect(client).not_to receive(:close)
257
+ perform
258
+ end
259
+
260
+ it "closes the connection after each raiseed attempt" do
261
+ expect(client).to receive(:close).exactly(3).times
262
+ begin
263
+ perform(should_retry: ->(*) { true }, tries: 3)
264
+ rescue HTTP::OutOfRetriesError
265
+ end
266
+ end
267
+
268
+ it "closes the connection on an unexpected exception" do
269
+ expect(client).to receive(:close)
270
+ begin
271
+ perform do
272
+ raise CustomException
273
+ end
274
+ rescue CustomException
275
+ end
276
+ end
277
+ end
278
+
279
+ describe HTTP::OutOfRetriesError do
280
+ it "has the original exception as a cause if available" do
281
+ err = nil
282
+ begin
283
+ perform(exceptions: [CustomException]) do
284
+ raise CustomException
285
+ end
286
+ rescue described_class => e
287
+ err = e
288
+ end
289
+ expect(err.cause).to be_a CustomException
290
+ end
291
+
292
+ it "has the last raiseed response as an attribute" do
293
+ err = nil
294
+ begin
295
+ perform(should_retry: ->(*) { true })
296
+ rescue described_class => e
297
+ err = e
298
+ end
299
+ expect(err.response).to be response
300
+ end
301
+ end
302
+ end
@@ -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
@@ -152,6 +152,35 @@ RSpec.describe HTTP do
152
152
  end
153
153
  end
154
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
+
155
184
  context "posting forms to resources" do
156
185
  it "is easy" do
157
186
  response = HTTP.post "#{dummy.endpoint}/form", :form => {:example => "testing-form"}
@@ -460,20 +489,37 @@ RSpec.describe HTTP do
460
489
 
461
490
  context "with :normalize_uri" do
462
491
  it "normalizes URI" do
463
- response = HTTP.get "#{dummy.endpoint}/hello world"
492
+ response = HTTP.get "#{dummy.endpoint}/héllö-wörld"
464
493
  expect(response.to_s).to eq("hello world")
465
494
  end
466
495
 
467
496
  it "uses the custom URI Normalizer method" do
468
497
  client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc})
469
- response = client.get("#{dummy.endpoint}/hello world")
498
+ response = client.get("#{dummy.endpoint}/héllö-wörld")
470
499
  expect(response.status).to eq(400)
471
500
  end
472
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
+
473
519
  it "uses the default URI normalizer" do
474
520
  client = HTTP.use :normalize_uri
475
521
  expect(HTTP::URI::NORMALIZER).to receive(:call).and_call_original
476
- response = client.get("#{dummy.endpoint}/hello world")
522
+ response = client.get("#{dummy.endpoint}/héllö-wörld")
477
523
  expect(response.to_s).to eq("hello world")
478
524
  end
479
525
  end
data/spec/spec_helper.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "./support/fuubar" unless ENV["CI"]
5
5
 
6
6
  require "http"
7
7
  require "rspec/its"
8
+ require "rspec/memory"
8
9
  require "support/capture_warning"
9
10
  require "support/fakeio"
10
11