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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +67 -0
- data/.gitignore +6 -9
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/metrics.yml +4 -0
- data/.rubocop/rspec.yml +9 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +9 -108
- data/.rubocop_todo.yml +219 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +67 -0
- data/{CHANGES.md → CHANGES_OLD.md} +358 -0
- data/Gemfile +19 -10
- data/LICENSE.txt +1 -1
- data/README.md +53 -85
- data/Rakefile +3 -11
- data/SECURITY.md +17 -0
- data/http.gemspec +15 -6
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +71 -41
- data/lib/http/client.rb +73 -52
- data/lib/http/connection.rb +28 -18
- data/lib/http/content_type.rb +12 -7
- data/lib/http/errors.rb +19 -0
- data/lib/http/feature.rb +18 -1
- data/lib/http/features/auto_deflate.rb +27 -6
- data/lib/http/features/auto_inflate.rb +32 -6
- data/lib/http/features/instrumentation.rb +69 -0
- data/lib/http/features/logging.rb +53 -0
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +72 -49
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +31 -28
- data/lib/http/redirector.rb +56 -4
- data/lib/http/request/body.rb +31 -0
- data/lib/http/request/writer.rb +29 -9
- data/lib/http/request.rb +76 -41
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +78 -26
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +45 -27
- data/lib/http/retriable/client.rb +37 -0
- data/lib/http/retriable/delay_calculator.rb +64 -0
- data/lib/http/retriable/errors.rb +14 -0
- data/lib/http/retriable/performer.rb +153 -0
- data/lib/http/timeout/global.rb +29 -47
- data/lib/http/timeout/null.rb +12 -8
- data/lib/http/timeout/per_operation.rb +32 -57
- data/lib/http/uri.rb +75 -1
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +2 -2
- data/spec/lib/http/client_spec.rb +189 -36
- data/spec/lib/http/connection_spec.rb +31 -6
- data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
- data/spec/lib/http/features/instrumentation_spec.rb +81 -0
- data/spec/lib/http/features/logging_spec.rb +65 -0
- data/spec/lib/http/features/raise_error_spec.rb +62 -0
- data/spec/lib/http/headers/normalizer_spec.rb +52 -0
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +6 -2
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +147 -3
- data/spec/lib/http/request/body_spec.rb +71 -4
- data/spec/lib/http/request/writer_spec.rb +45 -2
- data/spec/lib/http/request_spec.rb +11 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +74 -0
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +83 -7
- data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
- data/spec/lib/http/retriable/performer_spec.rb +302 -0
- data/spec/lib/http/uri/normalizer_spec.rb +95 -0
- data/spec/lib/http/uri_spec.rb +39 -0
- data/spec/lib/http_spec.rb +121 -68
- data/spec/regression_specs.rb +7 -0
- data/spec/spec_helper.rb +22 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +42 -11
- data/spec/support/dummy_server.rb +9 -8
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +62 -66
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +66 -27
- data/.coveralls.yml +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -36
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
1
|
# coding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "logger"
|
|
3
6
|
|
|
4
7
|
require "support/http_handling_shared"
|
|
5
8
|
require "support/dummy_server"
|
|
@@ -8,39 +11,50 @@ require "support/ssl_helper"
|
|
|
8
11
|
RSpec.describe HTTP::Client do
|
|
9
12
|
run_server(:dummy) { DummyServer.new }
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
before do
|
|
15
|
+
stubbed_client = Class.new(HTTP::Client) do
|
|
16
|
+
def perform(request, options)
|
|
17
|
+
stubbed = stubs[HTTP::URI::NORMALIZER.call(request.uri).to_s]
|
|
18
|
+
stubbed ? stubbed.call(request) : super(request, options)
|
|
19
|
+
end
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
def stubs
|
|
22
|
+
@stubs ||= {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stub(stubs)
|
|
26
|
+
@stubs = stubs.transform_keys do |k|
|
|
27
|
+
HTTP::URI::NORMALIZER.call(k).to_s
|
|
28
|
+
end
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
@stubs = stubs.each_with_object({}) do |(k, v), o|
|
|
22
|
-
o[HTTP::URI.parse k] = v
|
|
30
|
+
self
|
|
23
31
|
end
|
|
32
|
+
end
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
def redirect_response(location, status = 302)
|
|
35
|
+
lambda do |request|
|
|
36
|
+
HTTP::Response.new(
|
|
37
|
+
:status => status,
|
|
38
|
+
:version => "1.1",
|
|
39
|
+
:headers => {"Location" => location},
|
|
40
|
+
:body => "",
|
|
41
|
+
:request => request
|
|
42
|
+
)
|
|
43
|
+
end
|
|
26
44
|
end
|
|
27
|
-
end
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
def simple_response(body, status = 200)
|
|
47
|
+
lambda do |request|
|
|
48
|
+
HTTP::Response.new(
|
|
49
|
+
:status => status,
|
|
50
|
+
:version => "1.1",
|
|
51
|
+
:body => body,
|
|
52
|
+
:request => request
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
37
56
|
|
|
38
|
-
|
|
39
|
-
HTTP::Response.new(
|
|
40
|
-
:status => status,
|
|
41
|
-
:version => "1.1",
|
|
42
|
-
:body => body
|
|
43
|
-
)
|
|
57
|
+
stub_const("StubbedClient", stubbed_client)
|
|
44
58
|
end
|
|
45
59
|
|
|
46
60
|
describe "following redirects" do
|
|
@@ -98,13 +112,45 @@ RSpec.describe HTTP::Client do
|
|
|
98
112
|
end
|
|
99
113
|
|
|
100
114
|
it "works like a charm in real world" do
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
expect(client.get(url).to_s).to include "support for non-ascii URIs"
|
|
115
|
+
expect(HTTP.follow.get("https://bit.ly/2UaBT4R").parse(:json)).
|
|
116
|
+
to include("url" => "https://httpbin.org/anything/könig")
|
|
104
117
|
end
|
|
105
118
|
end
|
|
106
119
|
end
|
|
107
120
|
|
|
121
|
+
describe "following redirects with logging" do
|
|
122
|
+
let(:logger) do
|
|
123
|
+
logger = Logger.new(logdev)
|
|
124
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
125
|
+
logger.level = Logger::INFO
|
|
126
|
+
logger
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
let(:logdev) { StringIO.new }
|
|
130
|
+
|
|
131
|
+
it "logs all requests" do
|
|
132
|
+
client = StubbedClient.new(:follow => true, :features => { :logging => { :logger => logger } }).stub(
|
|
133
|
+
"http://example.com/" => redirect_response("/1"),
|
|
134
|
+
"http://example.com/1" => redirect_response("/2"),
|
|
135
|
+
"http://example.com/2" => redirect_response("/3"),
|
|
136
|
+
"http://example.com/3" => simple_response("OK")
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
expect { client.get("http://example.com/") }.not_to raise_error
|
|
140
|
+
|
|
141
|
+
expect(logdev.string).to eq <<~OUTPUT
|
|
142
|
+
** INFO **
|
|
143
|
+
> GET http://example.com/
|
|
144
|
+
** INFO **
|
|
145
|
+
> GET http://example.com/1
|
|
146
|
+
** INFO **
|
|
147
|
+
> GET http://example.com/2
|
|
148
|
+
** INFO **
|
|
149
|
+
> GET http://example.com/3
|
|
150
|
+
OUTPUT
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
108
154
|
describe "parsing params" do
|
|
109
155
|
let(:client) { HTTP::Client.new }
|
|
110
156
|
before { allow(client).to receive :perform }
|
|
@@ -190,6 +236,22 @@ RSpec.describe HTTP::Client do
|
|
|
190
236
|
|
|
191
237
|
client.get("http://example.com/", :form => {:foo => HTTP::FormData::Part.new("content")})
|
|
192
238
|
end
|
|
239
|
+
|
|
240
|
+
context "when passing an HTTP::FormData object directly" do
|
|
241
|
+
it "creates url encoded form data object" do
|
|
242
|
+
client = HTTP::Client.new
|
|
243
|
+
form_data = HTTP::FormData::Multipart.new({ :foo => "bar" })
|
|
244
|
+
|
|
245
|
+
allow(client).to receive(:perform)
|
|
246
|
+
|
|
247
|
+
expect(HTTP::Request).to receive(:new) do |opts|
|
|
248
|
+
expect(opts[:body]).to be form_data
|
|
249
|
+
expect(opts[:body].to_s).to match(/^Content-Disposition: form-data; name="foo"\r\n\r\nbar\r\n/m)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
client.get("http://example.com/", :form => form_data)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
193
255
|
end
|
|
194
256
|
|
|
195
257
|
describe "passing json" do
|
|
@@ -199,6 +261,7 @@ RSpec.describe HTTP::Client do
|
|
|
199
261
|
|
|
200
262
|
expect(HTTP::Request).to receive(:new) do |opts|
|
|
201
263
|
expect(opts[:body]).to eq '{"foo":"bar"}'
|
|
264
|
+
expect(opts[:headers]["Content-Type"]).to eq "application/json; charset=utf-8"
|
|
202
265
|
end
|
|
203
266
|
|
|
204
267
|
client.get("http://example.com/", :json => {:foo => :bar})
|
|
@@ -213,9 +276,9 @@ RSpec.describe HTTP::Client do
|
|
|
213
276
|
end
|
|
214
277
|
|
|
215
278
|
it "works like a charm in real world" do
|
|
216
|
-
url
|
|
217
|
-
|
|
218
|
-
expect(
|
|
279
|
+
url = "https://httpbin.org/anything/ö無"
|
|
280
|
+
|
|
281
|
+
expect(HTTP.follow.get(url).parse(:json)).to include("url" => url)
|
|
219
282
|
end
|
|
220
283
|
end
|
|
221
284
|
|
|
@@ -234,7 +297,7 @@ RSpec.describe HTTP::Client do
|
|
|
234
297
|
|
|
235
298
|
context "when :auto_deflate was specified" do
|
|
236
299
|
let(:headers) { {"Content-Length" => "12"} }
|
|
237
|
-
let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}} }
|
|
300
|
+
let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}}, :body => "foo" }
|
|
238
301
|
|
|
239
302
|
it "deletes Content-Length header" do
|
|
240
303
|
expect(client).to receive(:perform) do |req, _|
|
|
@@ -251,6 +314,87 @@ RSpec.describe HTTP::Client do
|
|
|
251
314
|
|
|
252
315
|
client.request(:get, "http://example.com/")
|
|
253
316
|
end
|
|
317
|
+
|
|
318
|
+
context "and there is no body" do
|
|
319
|
+
let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}} }
|
|
320
|
+
|
|
321
|
+
it "doesn't set Content-Encoding header" do
|
|
322
|
+
expect(client).to receive(:perform) do |req, _|
|
|
323
|
+
expect(req.headers).not_to include "Content-Encoding"
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
client.request(:get, "http://example.com/")
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
context "Feature" do
|
|
332
|
+
let(:feature_class) do
|
|
333
|
+
Class.new(HTTP::Feature) do
|
|
334
|
+
attr_reader :captured_request, :captured_response, :captured_error
|
|
335
|
+
|
|
336
|
+
def wrap_request(request)
|
|
337
|
+
@captured_request = request
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def wrap_response(response)
|
|
341
|
+
@captured_response = response
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def on_error(request, error)
|
|
345
|
+
@captured_request = request
|
|
346
|
+
@captured_error = error
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it "is given a chance to wrap the Request" do
|
|
352
|
+
feature_instance = feature_class.new
|
|
353
|
+
|
|
354
|
+
response = client.use(:test_feature => feature_instance).
|
|
355
|
+
request(:get, dummy.endpoint)
|
|
356
|
+
|
|
357
|
+
expect(response.code).to eq(200)
|
|
358
|
+
expect(feature_instance.captured_request.verb).to eq(:get)
|
|
359
|
+
expect(feature_instance.captured_request.uri.to_s).to eq("#{dummy.endpoint}/")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it "is given a chance to wrap the Response" do
|
|
363
|
+
feature_instance = feature_class.new
|
|
364
|
+
|
|
365
|
+
response = client.use(:test_feature => feature_instance).
|
|
366
|
+
request(:get, dummy.endpoint)
|
|
367
|
+
|
|
368
|
+
expect(feature_instance.captured_response).to eq(response)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
it "is given a chance to handle an error" do
|
|
372
|
+
sleep_url = "#{dummy.endpoint}/sleep"
|
|
373
|
+
feature_instance = feature_class.new
|
|
374
|
+
|
|
375
|
+
expect do
|
|
376
|
+
client.use(:test_feature => feature_instance).
|
|
377
|
+
timeout(0.2).
|
|
378
|
+
request(:post, sleep_url)
|
|
379
|
+
end.to raise_error(HTTP::TimeoutError)
|
|
380
|
+
|
|
381
|
+
expect(feature_instance.captured_error).to be_a(HTTP::TimeoutError)
|
|
382
|
+
expect(feature_instance.captured_request.verb).to eq(:post)
|
|
383
|
+
expect(feature_instance.captured_request.uri.to_s).to eq(sleep_url)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
it "is given a chance to handle a connection timeout error" do
|
|
387
|
+
allow(TCPSocket).to receive(:open) { sleep 1 }
|
|
388
|
+
sleep_url = "#{dummy.endpoint}/sleep"
|
|
389
|
+
feature_instance = feature_class.new
|
|
390
|
+
|
|
391
|
+
expect do
|
|
392
|
+
client.use(:test_feature => feature_instance).
|
|
393
|
+
timeout(0.001).
|
|
394
|
+
request(:post, sleep_url)
|
|
395
|
+
end.to raise_error(HTTP::ConnectTimeoutError)
|
|
396
|
+
expect(feature_instance.captured_error).to be_a(HTTP::ConnectTimeoutError)
|
|
397
|
+
end
|
|
254
398
|
end
|
|
255
399
|
end
|
|
256
400
|
|
|
@@ -261,7 +405,8 @@ RSpec.describe HTTP::Client do
|
|
|
261
405
|
let(:client) { described_class.new(options.merge(extra_options)) }
|
|
262
406
|
end
|
|
263
407
|
|
|
264
|
-
|
|
408
|
+
# TODO: https://github.com/httprb/http/issues/627
|
|
409
|
+
xdescribe "working with SSL" do
|
|
265
410
|
run_server(:dummy_ssl) { DummyServer.new(:ssl => true) }
|
|
266
411
|
|
|
267
412
|
let(:extra_options) { {} }
|
|
@@ -304,6 +449,14 @@ RSpec.describe HTTP::Client do
|
|
|
304
449
|
client.get(dummy.endpoint).to_s
|
|
305
450
|
end
|
|
306
451
|
|
|
452
|
+
it "provides access to the Request from the Response" do
|
|
453
|
+
unique_value = "20190424"
|
|
454
|
+
response = client.headers("X-Value" => unique_value).get(dummy.endpoint)
|
|
455
|
+
|
|
456
|
+
expect(response.request).to be_a(HTTP::Request)
|
|
457
|
+
expect(response.request.headers["X-Value"]).to eq(unique_value)
|
|
458
|
+
end
|
|
459
|
+
|
|
307
460
|
context "with HEAD request" do
|
|
308
461
|
it "does not iterates through body" do
|
|
309
462
|
expect_any_instance_of(HTTP::Connection).to_not receive(:readpartial)
|
|
@@ -394,7 +547,7 @@ RSpec.describe HTTP::Client do
|
|
|
394
547
|
BODY
|
|
395
548
|
end
|
|
396
549
|
|
|
397
|
-
|
|
550
|
+
xit "raises HTTP::ConnectionError" do
|
|
398
551
|
expect { client.get(dummy.endpoint).to_s }.to raise_error(HTTP::ConnectionError)
|
|
399
552
|
end
|
|
400
553
|
end
|
|
@@ -3,16 +3,36 @@
|
|
|
3
3
|
RSpec.describe HTTP::Connection do
|
|
4
4
|
let(:req) do
|
|
5
5
|
HTTP::Request.new(
|
|
6
|
-
:verb
|
|
7
|
-
:uri
|
|
8
|
-
:headers
|
|
6
|
+
:verb => :get,
|
|
7
|
+
:uri => "http://example.com/",
|
|
8
|
+
:headers => {}
|
|
9
9
|
)
|
|
10
10
|
end
|
|
11
|
-
let(:socket) { double(:connect => nil) }
|
|
11
|
+
let(:socket) { double(:connect => nil, :close => nil) }
|
|
12
12
|
let(:timeout_class) { double(:new => socket) }
|
|
13
13
|
let(:opts) { HTTP::Options.new(:timeout_class => timeout_class) }
|
|
14
14
|
let(:connection) { HTTP::Connection.new(req, opts) }
|
|
15
15
|
|
|
16
|
+
describe "#initialize times out" do
|
|
17
|
+
let(:req) do
|
|
18
|
+
HTTP::Request.new(
|
|
19
|
+
:verb => :get,
|
|
20
|
+
:uri => "https://example.com/",
|
|
21
|
+
:headers => {}
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
before do
|
|
26
|
+
expect(socket).to receive(:start_tls).and_raise(HTTP::TimeoutError)
|
|
27
|
+
expect(socket).to receive(:closed?) { false }
|
|
28
|
+
expect(socket).to receive(:close)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "closes the connection" do
|
|
32
|
+
expect { connection }.to raise_error(HTTP::TimeoutError)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
16
36
|
describe "#read_headers!" do
|
|
17
37
|
before do
|
|
18
38
|
connection.instance_variable_set(:@pending_response, true)
|
|
@@ -20,14 +40,17 @@ RSpec.describe HTTP::Connection do
|
|
|
20
40
|
<<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n")
|
|
21
41
|
| HTTP/1.1 200 OK
|
|
22
42
|
| Content-Type: text
|
|
43
|
+
| foo_bar: 123
|
|
23
44
|
|
|
|
24
45
|
RESPONSE
|
|
25
46
|
end
|
|
26
47
|
end
|
|
27
48
|
|
|
28
|
-
it "
|
|
49
|
+
it "populates headers collection, preserving casing" do
|
|
29
50
|
connection.read_headers!
|
|
30
|
-
expect(connection.headers).to eq("Content-Type" => "text")
|
|
51
|
+
expect(connection.headers).to eq("Content-Type" => "text", "foo_bar" => "123")
|
|
52
|
+
expect(connection.headers["Foo-Bar"]).to eq("123")
|
|
53
|
+
expect(connection.headers["foo_bar"]).to eq("123")
|
|
31
54
|
end
|
|
32
55
|
end
|
|
33
56
|
|
|
@@ -55,9 +78,11 @@ RSpec.describe HTTP::Connection do
|
|
|
55
78
|
connection.read_headers!
|
|
56
79
|
buffer = String.new
|
|
57
80
|
while (s = connection.readpartial(3))
|
|
81
|
+
expect(connection.finished_request?).to be false if s != ""
|
|
58
82
|
buffer << s
|
|
59
83
|
end
|
|
60
84
|
expect(buffer).to eq "1234567890"
|
|
85
|
+
expect(connection.finished_request?).to be true
|
|
61
86
|
end
|
|
62
87
|
end
|
|
63
88
|
end
|
|
@@ -1,68 +1,85 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
RSpec.describe HTTP::Features::AutoInflate do
|
|
4
|
-
subject { HTTP::Features::AutoInflate.new }
|
|
4
|
+
subject(:feature) { HTTP::Features::AutoInflate.new }
|
|
5
|
+
|
|
5
6
|
let(:connection) { double }
|
|
6
|
-
let(:headers)
|
|
7
|
+
let(:headers) { {} }
|
|
8
|
+
|
|
7
9
|
let(:response) do
|
|
8
10
|
HTTP::Response.new(
|
|
9
11
|
:version => "1.1",
|
|
10
12
|
:status => 200,
|
|
11
13
|
:headers => headers,
|
|
12
|
-
:connection => connection
|
|
14
|
+
:connection => connection,
|
|
15
|
+
:request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
|
|
13
16
|
)
|
|
14
17
|
end
|
|
15
18
|
|
|
16
|
-
describe "
|
|
19
|
+
describe "#wrap_response" do
|
|
20
|
+
subject(:result) { feature.wrap_response(response) }
|
|
21
|
+
|
|
17
22
|
context "when there is no Content-Encoding header" do
|
|
18
|
-
it "returns
|
|
19
|
-
|
|
20
|
-
expect(stream).to eq(connection)
|
|
23
|
+
it "returns original request" do
|
|
24
|
+
expect(result).to be response
|
|
21
25
|
end
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
context "for identity Content-Encoding header" do
|
|
25
|
-
let(:headers) { {:content_encoding => "
|
|
29
|
+
let(:headers) { {:content_encoding => "identity"} }
|
|
26
30
|
|
|
27
|
-
it "returns
|
|
28
|
-
|
|
29
|
-
expect(stream).to eq(connection)
|
|
31
|
+
it "returns original request" do
|
|
32
|
+
expect(result).to be response
|
|
30
33
|
end
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
context "for unknown Content-Encoding header" do
|
|
34
37
|
let(:headers) { {:content_encoding => "not-supported"} }
|
|
35
38
|
|
|
36
|
-
it "returns
|
|
37
|
-
|
|
38
|
-
expect(stream).to eq(connection)
|
|
39
|
+
it "returns original request" do
|
|
40
|
+
expect(result).to be response
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
context "for deflate Content-Encoding header" do
|
|
43
45
|
let(:headers) { {:content_encoding => "deflate"} }
|
|
44
46
|
|
|
45
|
-
it "returns HTTP::Response
|
|
46
|
-
|
|
47
|
-
expect(stream).to be_instance_of HTTP::Response::Inflater
|
|
47
|
+
it "returns a HTTP::Response wrapping the inflated response body" do
|
|
48
|
+
expect(result.body).to be_instance_of HTTP::Response::Body
|
|
48
49
|
end
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
context "for gzip Content-Encoding header" do
|
|
52
53
|
let(:headers) { {:content_encoding => "gzip"} }
|
|
53
54
|
|
|
54
|
-
it "returns HTTP::Response
|
|
55
|
-
|
|
56
|
-
expect(stream).to be_instance_of HTTP::Response::Inflater
|
|
55
|
+
it "returns a HTTP::Response wrapping the inflated response body" do
|
|
56
|
+
expect(result.body).to be_instance_of HTTP::Response::Body
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
context "for x-gzip Content-Encoding header" do
|
|
61
61
|
let(:headers) { {:content_encoding => "x-gzip"} }
|
|
62
62
|
|
|
63
|
-
it "returns HTTP::Response
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
it "returns a HTTP::Response wrapping the inflated response body" do
|
|
64
|
+
expect(result.body).to be_instance_of HTTP::Response::Body
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# TODO(ixti): We should refactor API to either make uri non-optional,
|
|
69
|
+
# or add reference to request into response object (better).
|
|
70
|
+
context "when response has uri" do
|
|
71
|
+
let(:response) do
|
|
72
|
+
HTTP::Response.new(
|
|
73
|
+
:version => "1.1",
|
|
74
|
+
:status => 200,
|
|
75
|
+
:headers => {:content_encoding => "gzip"},
|
|
76
|
+
:connection => connection,
|
|
77
|
+
:request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "preserves uri in wrapped response" do
|
|
82
|
+
expect(result.uri).to eq HTTP::URI.parse("https://example.com")
|
|
66
83
|
end
|
|
67
84
|
end
|
|
68
85
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe HTTP::Features::Instrumentation do
|
|
4
|
+
subject(:feature) { HTTP::Features::Instrumentation.new(:instrumenter => instrumenter) }
|
|
5
|
+
|
|
6
|
+
let(:instrumenter) { TestInstrumenter.new }
|
|
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
|
+
|
|
28
|
+
describe "logging the request" do
|
|
29
|
+
let(:request) do
|
|
30
|
+
HTTP::Request.new(
|
|
31
|
+
:verb => :post,
|
|
32
|
+
:uri => "https://example.com/",
|
|
33
|
+
:headers => {:accept => "application/json"},
|
|
34
|
+
:body => '{"hello": "world!"}'
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "should log the request" do
|
|
39
|
+
feature.wrap_request(request)
|
|
40
|
+
|
|
41
|
+
expect(instrumenter.output[:start]).to eq(:request => request)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe "logging the response" do
|
|
46
|
+
let(:response) do
|
|
47
|
+
HTTP::Response.new(
|
|
48
|
+
:version => "1.1",
|
|
49
|
+
:status => 200,
|
|
50
|
+
:headers => {:content_type => "application/json"},
|
|
51
|
+
:body => '{"success": true}',
|
|
52
|
+
:request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "should log the response" do
|
|
57
|
+
feature.wrap_response(response)
|
|
58
|
+
|
|
59
|
+
expect(instrumenter.output[:finish]).to eq(:response => response)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe "logging errors" do
|
|
64
|
+
let(:request) do
|
|
65
|
+
HTTP::Request.new(
|
|
66
|
+
:verb => :post,
|
|
67
|
+
:uri => "https://example.com/",
|
|
68
|
+
:headers => {:accept => "application/json"},
|
|
69
|
+
:body => '{"hello": "world!"}'
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
let(:error) { HTTP::TimeoutError.new }
|
|
74
|
+
|
|
75
|
+
it "should log the error" do
|
|
76
|
+
feature.on_error(request, error)
|
|
77
|
+
|
|
78
|
+
expect(instrumenter.output[:finish]).to eq(:request => request, :error => error)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
RSpec.describe HTTP::Features::Logging do
|
|
6
|
+
subject(:feature) do
|
|
7
|
+
logger = Logger.new(logdev)
|
|
8
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
9
|
+
|
|
10
|
+
described_class.new(:logger => logger)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
let(:logdev) { StringIO.new }
|
|
14
|
+
|
|
15
|
+
describe "logging the request" do
|
|
16
|
+
let(:request) do
|
|
17
|
+
HTTP::Request.new(
|
|
18
|
+
:verb => :post,
|
|
19
|
+
:uri => "https://example.com/",
|
|
20
|
+
:headers => {:accept => "application/json"},
|
|
21
|
+
:body => '{"hello": "world!"}'
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "should log the request" do
|
|
26
|
+
feature.wrap_request(request)
|
|
27
|
+
|
|
28
|
+
expect(logdev.string).to eq <<~OUTPUT
|
|
29
|
+
** INFO **
|
|
30
|
+
> POST https://example.com/
|
|
31
|
+
** DEBUG **
|
|
32
|
+
Accept: application/json
|
|
33
|
+
Host: example.com
|
|
34
|
+
User-Agent: http.rb/#{HTTP::VERSION}
|
|
35
|
+
|
|
36
|
+
{"hello": "world!"}
|
|
37
|
+
OUTPUT
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe "logging the response" do
|
|
42
|
+
let(:response) do
|
|
43
|
+
HTTP::Response.new(
|
|
44
|
+
:version => "1.1",
|
|
45
|
+
:status => 200,
|
|
46
|
+
:headers => {:content_type => "application/json"},
|
|
47
|
+
:body => '{"success": true}',
|
|
48
|
+
:request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "should log the response" do
|
|
53
|
+
feature.wrap_response(response)
|
|
54
|
+
|
|
55
|
+
expect(logdev.string).to eq <<~OUTPUT
|
|
56
|
+
** INFO **
|
|
57
|
+
< 200 OK
|
|
58
|
+
** DEBUG **
|
|
59
|
+
Content-Type: application/json
|
|
60
|
+
|
|
61
|
+
{"success": true}
|
|
62
|
+
OUTPUT
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|