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
@@ -11,8 +11,6 @@ module HTTP
|
|
11
11
|
WRITE_TIMEOUT = 0.25
|
12
12
|
READ_TIMEOUT = 0.25
|
13
13
|
|
14
|
-
attr_reader :read_timeout, :write_timeout, :connect_timeout
|
15
|
-
|
16
14
|
def initialize(*args)
|
17
15
|
super
|
18
16
|
|
@@ -22,7 +20,7 @@ module HTTP
|
|
22
20
|
end
|
23
21
|
|
24
22
|
def connect(socket_class, host, port, nodelay = false)
|
25
|
-
::Timeout.timeout(connect_timeout, TimeoutError) do
|
23
|
+
::Timeout.timeout(@connect_timeout, TimeoutError) do
|
26
24
|
@socket = socket_class.open(host, port)
|
27
25
|
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
28
26
|
end
|
@@ -67,7 +65,7 @@ module HTTP
|
|
67
65
|
return :eof if result.nil?
|
68
66
|
return result if result != :wait_readable
|
69
67
|
|
70
|
-
raise TimeoutError, "Read timed out after #{read_timeout} seconds" if timeout
|
68
|
+
raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
|
71
69
|
# marking the socket for timeout. Why is this not being raised immediately?
|
72
70
|
# it seems there is some race-condition on the network level between calling
|
73
71
|
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
|
@@ -78,7 +76,7 @@ module HTTP
|
|
78
76
|
# timeout. Else, the first timeout was a proper timeout.
|
79
77
|
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
|
80
78
|
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
|
81
|
-
timeout = true unless @socket.to_io.wait_readable(read_timeout)
|
79
|
+
timeout = true unless @socket.to_io.wait_readable(@read_timeout)
|
82
80
|
end
|
83
81
|
end
|
84
82
|
|
@@ -89,9 +87,9 @@ module HTTP
|
|
89
87
|
result = @socket.write_nonblock(data, :exception => false)
|
90
88
|
return result unless result == :wait_writable
|
91
89
|
|
92
|
-
raise TimeoutError, "Write timed out after #{write_timeout} seconds" if timeout
|
90
|
+
raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
|
93
91
|
|
94
|
-
timeout = true unless @socket.to_io.wait_writable(write_timeout)
|
92
|
+
timeout = true unless @socket.to_io.wait_writable(@write_timeout)
|
95
93
|
end
|
96
94
|
end
|
97
95
|
|
data/lib/http/uri.rb
CHANGED
@@ -26,6 +26,19 @@ module HTTP
|
|
26
26
|
# @private
|
27
27
|
HTTPS_SCHEME = "https"
|
28
28
|
|
29
|
+
# @private
|
30
|
+
NORMALIZER = lambda do |uri|
|
31
|
+
uri = HTTP::URI.parse uri
|
32
|
+
|
33
|
+
HTTP::URI.new(
|
34
|
+
:scheme => uri.normalized_scheme,
|
35
|
+
:authority => uri.normalized_authority,
|
36
|
+
:path => uri.normalized_path,
|
37
|
+
:query => uri.query,
|
38
|
+
:fragment => uri.normalized_fragment
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
29
42
|
# Parse the given URI string, returning an HTTP::URI object
|
30
43
|
#
|
31
44
|
# @param [HTTP::URI, String, #to_str] uri to parse
|
data/lib/http/version.rb
CHANGED
@@ -98,9 +98,8 @@ RSpec.describe HTTP::Client do
|
|
98
98
|
end
|
99
99
|
|
100
100
|
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"
|
101
|
+
expect(HTTP.follow.get("https://bit.ly/2UaBT4R").parse(:json)).
|
102
|
+
to include("url" => "https://httpbin.org/anything/könig")
|
104
103
|
end
|
105
104
|
end
|
106
105
|
end
|
@@ -190,6 +189,22 @@ RSpec.describe HTTP::Client do
|
|
190
189
|
|
191
190
|
client.get("http://example.com/", :form => {:foo => HTTP::FormData::Part.new("content")})
|
192
191
|
end
|
192
|
+
|
193
|
+
context "when passing an HTTP::FormData object directly" do
|
194
|
+
it "creates url encoded form data object" do
|
195
|
+
client = HTTP::Client.new
|
196
|
+
form_data = HTTP::FormData::Multipart.new(:foo => "bar")
|
197
|
+
|
198
|
+
allow(client).to receive(:perform)
|
199
|
+
|
200
|
+
expect(HTTP::Request).to receive(:new) do |opts|
|
201
|
+
expect(opts[:body]).to be form_data
|
202
|
+
expect(opts[:body].to_s).to match(/^Content-Disposition: form-data; name="foo"\r\n\r\nbar\r\n/m)
|
203
|
+
end
|
204
|
+
|
205
|
+
client.get("http://example.com/", :form => form_data)
|
206
|
+
end
|
207
|
+
end
|
193
208
|
end
|
194
209
|
|
195
210
|
describe "passing json" do
|
@@ -213,9 +228,9 @@ RSpec.describe HTTP::Client do
|
|
213
228
|
end
|
214
229
|
|
215
230
|
it "works like a charm in real world" do
|
216
|
-
url
|
217
|
-
|
218
|
-
expect(
|
231
|
+
url = "https://httpbin.org/anything/ö無"
|
232
|
+
|
233
|
+
expect(HTTP.follow.get(url).parse(:json)).to include("url" => url)
|
219
234
|
end
|
220
235
|
end
|
221
236
|
|
@@ -234,7 +249,7 @@ RSpec.describe HTTP::Client do
|
|
234
249
|
|
235
250
|
context "when :auto_deflate was specified" do
|
236
251
|
let(:headers) { {"Content-Length" => "12"} }
|
237
|
-
let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}} }
|
252
|
+
let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}}, :body => "foo" }
|
238
253
|
|
239
254
|
it "deletes Content-Length header" do
|
240
255
|
expect(client).to receive(:perform) do |req, _|
|
@@ -251,6 +266,18 @@ RSpec.describe HTTP::Client do
|
|
251
266
|
|
252
267
|
client.request(:get, "http://example.com/")
|
253
268
|
end
|
269
|
+
|
270
|
+
context "and there is no body" do
|
271
|
+
let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}} }
|
272
|
+
|
273
|
+
it "doesn't set Content-Encoding header" do
|
274
|
+
expect(client).to receive(:perform) do |req, _|
|
275
|
+
expect(req.headers).not_to include "Content-Encoding"
|
276
|
+
end
|
277
|
+
|
278
|
+
client.request(:get, "http://example.com/")
|
279
|
+
end
|
280
|
+
end
|
254
281
|
end
|
255
282
|
end
|
256
283
|
|
@@ -1,9 +1,11 @@
|
|
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",
|
@@ -13,56 +15,70 @@ RSpec.describe HTTP::Features::AutoInflate do
|
|
13
15
|
)
|
14
16
|
end
|
15
17
|
|
16
|
-
describe "
|
18
|
+
describe "#wrap_response" do
|
19
|
+
subject(:result) { feature.wrap_response(response) }
|
20
|
+
|
17
21
|
context "when there is no Content-Encoding header" do
|
18
|
-
it "returns
|
19
|
-
|
20
|
-
expect(stream).to eq(connection)
|
22
|
+
it "returns original request" do
|
23
|
+
expect(result).to be response
|
21
24
|
end
|
22
25
|
end
|
23
26
|
|
24
27
|
context "for identity Content-Encoding header" do
|
25
|
-
let(:headers) { {:content_encoding => "
|
28
|
+
let(:headers) { {:content_encoding => "identity"} }
|
26
29
|
|
27
|
-
it "returns
|
28
|
-
|
29
|
-
expect(stream).to eq(connection)
|
30
|
+
it "returns original request" do
|
31
|
+
expect(result).to be response
|
30
32
|
end
|
31
33
|
end
|
32
34
|
|
33
35
|
context "for unknown Content-Encoding header" do
|
34
36
|
let(:headers) { {:content_encoding => "not-supported"} }
|
35
37
|
|
36
|
-
it "returns
|
37
|
-
|
38
|
-
expect(stream).to eq(connection)
|
38
|
+
it "returns original request" do
|
39
|
+
expect(result).to be response
|
39
40
|
end
|
40
41
|
end
|
41
42
|
|
42
43
|
context "for deflate Content-Encoding header" do
|
43
44
|
let(:headers) { {:content_encoding => "deflate"} }
|
44
45
|
|
45
|
-
it "returns HTTP::Response
|
46
|
-
|
47
|
-
expect(stream).to be_instance_of HTTP::Response::Inflater
|
46
|
+
it "returns a HTTP::Response wrapping the inflated response body" do
|
47
|
+
expect(result.body).to be_instance_of HTTP::Response::Body
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
51
|
context "for gzip Content-Encoding header" do
|
52
52
|
let(:headers) { {:content_encoding => "gzip"} }
|
53
53
|
|
54
|
-
it "returns HTTP::Response
|
55
|
-
|
56
|
-
expect(stream).to be_instance_of HTTP::Response::Inflater
|
54
|
+
it "returns a HTTP::Response wrapping the inflated response body" do
|
55
|
+
expect(result.body).to be_instance_of HTTP::Response::Body
|
57
56
|
end
|
58
57
|
end
|
59
58
|
|
60
59
|
context "for x-gzip Content-Encoding header" do
|
61
60
|
let(:headers) { {:content_encoding => "x-gzip"} }
|
62
61
|
|
63
|
-
it "returns HTTP::Response
|
64
|
-
|
65
|
-
|
62
|
+
it "returns a HTTP::Response wrapping the inflated response body" do
|
63
|
+
expect(result.body).to be_instance_of HTTP::Response::Body
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# TODO(ixti): We should refactor API to either make uri non-optional,
|
68
|
+
# or add reference to request into response object (better).
|
69
|
+
context "when response has uri" do
|
70
|
+
let(:response) do
|
71
|
+
HTTP::Response.new(
|
72
|
+
:version => "1.1",
|
73
|
+
:status => 200,
|
74
|
+
:headers => {:content_encoding => "gzip"},
|
75
|
+
:connection => connection,
|
76
|
+
:uri => "https://example.com"
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "preserves uri in wrapped response" do
|
81
|
+
expect(result.uri).to eq HTTP::URI.parse("https://example.com")
|
66
82
|
end
|
67
83
|
end
|
68
84
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Features::Instrumentation do
|
4
|
+
subject(:feature) { HTTP::Features::Instrumentation.new(:instrumenter => instrumenter) }
|
5
|
+
let(:instrumenter) { TestInstrumenter.new }
|
6
|
+
|
7
|
+
describe "logging the request" do
|
8
|
+
let(:request) do
|
9
|
+
HTTP::Request.new(
|
10
|
+
:verb => :post,
|
11
|
+
:uri => "https://example.com/",
|
12
|
+
:headers => {:accept => "application/json"},
|
13
|
+
:body => '{"hello": "world!"}'
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should log the request" do
|
18
|
+
feature.wrap_request(request)
|
19
|
+
|
20
|
+
expect(instrumenter.output[:start]).to eq(:request => request)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "logging the response" do
|
25
|
+
let(:response) do
|
26
|
+
HTTP::Response.new(
|
27
|
+
:version => "1.1",
|
28
|
+
:uri => "https://example.com",
|
29
|
+
:status => 200,
|
30
|
+
:headers => {:content_type => "application/json"},
|
31
|
+
:body => '{"success": true}'
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should log the response" do
|
36
|
+
feature.wrap_response(response)
|
37
|
+
|
38
|
+
expect(instrumenter.output[:finish]).to eq(:response => response)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class TestInstrumenter < HTTP::Features::Instrumentation::NullInstrumenter
|
43
|
+
attr_reader :output
|
44
|
+
def initialize
|
45
|
+
@output = {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def start(_name, payload)
|
49
|
+
output[:start] = payload
|
50
|
+
end
|
51
|
+
|
52
|
+
def finish(_name, payload)
|
53
|
+
output[:finish] = payload
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,67 @@
|
|
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) do
|
9
|
+
format("** %s **\n%s\n", severity, message)
|
10
|
+
end
|
11
|
+
|
12
|
+
described_class.new(:logger => logger)
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:logdev) { StringIO.new }
|
16
|
+
|
17
|
+
describe "logging the request" do
|
18
|
+
let(:request) do
|
19
|
+
HTTP::Request.new(
|
20
|
+
:verb => :post,
|
21
|
+
:uri => "https://example.com/",
|
22
|
+
:headers => {:accept => "application/json"},
|
23
|
+
:body => '{"hello": "world!"}'
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should log the request" do
|
28
|
+
feature.wrap_request(request)
|
29
|
+
|
30
|
+
expect(logdev.string).to eq <<~OUTPUT
|
31
|
+
** INFO **
|
32
|
+
> POST https://example.com/
|
33
|
+
** DEBUG **
|
34
|
+
Accept: application/json
|
35
|
+
Host: example.com
|
36
|
+
User-Agent: http.rb/#{HTTP::VERSION}
|
37
|
+
|
38
|
+
{"hello": "world!"}
|
39
|
+
OUTPUT
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "logging the response" do
|
44
|
+
let(:response) do
|
45
|
+
HTTP::Response.new(
|
46
|
+
:version => "1.1",
|
47
|
+
:uri => "https://example.com",
|
48
|
+
:status => 200,
|
49
|
+
:headers => {:content_type => "application/json"},
|
50
|
+
:body => '{"success": true}'
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should log the response" do
|
55
|
+
feature.wrap_response(response)
|
56
|
+
|
57
|
+
expect(logdev.string).to eq <<~OUTPUT
|
58
|
+
** INFO **
|
59
|
+
< 200 OK
|
60
|
+
** DEBUG **
|
61
|
+
Content-Type: application/json
|
62
|
+
|
63
|
+
{"success": true}
|
64
|
+
OUTPUT
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -75,6 +75,19 @@ RSpec.describe HTTP::Redirector do
|
|
75
75
|
expect(res.to_s).to eq "foo"
|
76
76
|
end
|
77
77
|
|
78
|
+
it "concatenates multiple Location headers" do
|
79
|
+
req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
|
80
|
+
headers = HTTP::Headers.new
|
81
|
+
|
82
|
+
%w[http://example.com /123].each { |loc| headers.add("Location", loc) }
|
83
|
+
|
84
|
+
res = redirector.perform(req, simple_response(301, "", headers)) do |redirect|
|
85
|
+
simple_response(200, redirect.uri.to_s)
|
86
|
+
end
|
87
|
+
|
88
|
+
expect(res.to_s).to eq "http://example.com/123"
|
89
|
+
end
|
90
|
+
|
78
91
|
context "following 300 redirect" do
|
79
92
|
context "with strict mode" do
|
80
93
|
let(:options) { {:strict => true} }
|
@@ -125,6 +125,28 @@ RSpec.describe HTTP::Request::Body do
|
|
125
125
|
end
|
126
126
|
end
|
127
127
|
|
128
|
+
context "when body is a pipe" do
|
129
|
+
let(:ios) { IO.pipe }
|
130
|
+
let(:body) { ios[0] }
|
131
|
+
|
132
|
+
around do |example|
|
133
|
+
writer = Thread.new(ios[1]) do |io|
|
134
|
+
io << "abcdef"
|
135
|
+
io.close
|
136
|
+
end
|
137
|
+
|
138
|
+
begin
|
139
|
+
example.run
|
140
|
+
ensure
|
141
|
+
writer.join
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
it "yields chunks of content" do
|
146
|
+
expect(chunks.inject("", :+)).to eq("abcdef")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
128
150
|
context "when body is an Enumerable IO" do
|
129
151
|
let(:data) { "a" * 16 * 1024 + "b" * 10 * 1024 }
|
130
152
|
let(:body) { StringIO.new data }
|
@@ -157,4 +179,33 @@ RSpec.describe HTTP::Request::Body do
|
|
157
179
|
end
|
158
180
|
end
|
159
181
|
end
|
182
|
+
|
183
|
+
describe "#==" do
|
184
|
+
context "when sources are equivalent" do
|
185
|
+
let(:body1) { HTTP::Request::Body.new("content") }
|
186
|
+
let(:body2) { HTTP::Request::Body.new("content") }
|
187
|
+
|
188
|
+
it "returns true" do
|
189
|
+
expect(body1).to eq body2
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context "when sources are not equivalent" do
|
194
|
+
let(:body1) { HTTP::Request::Body.new("content") }
|
195
|
+
let(:body2) { HTTP::Request::Body.new(nil) }
|
196
|
+
|
197
|
+
it "returns false" do
|
198
|
+
expect(body1).not_to eq body2
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "when objects are not of the same class" do
|
203
|
+
let(:body1) { HTTP::Request::Body.new("content") }
|
204
|
+
let(:body2) { "content" }
|
205
|
+
|
206
|
+
it "returns false" do
|
207
|
+
expect(body1).not_to eq body2
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
160
211
|
end
|