http 3.3.0 → 4.4.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|