http 3.3.0 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +3 -1
  4. data/.travis.yml +10 -7
  5. data/CHANGES.md +135 -0
  6. data/README.md +14 -10
  7. data/Rakefile +1 -1
  8. data/http.gemspec +12 -5
  9. data/lib/http.rb +1 -2
  10. data/lib/http/chainable.rb +20 -29
  11. data/lib/http/client.rb +25 -19
  12. data/lib/http/connection.rb +5 -9
  13. data/lib/http/feature.rb +14 -0
  14. data/lib/http/features/auto_deflate.rb +27 -6
  15. data/lib/http/features/auto_inflate.rb +33 -6
  16. data/lib/http/features/instrumentation.rb +64 -0
  17. data/lib/http/features/logging.rb +55 -0
  18. data/lib/http/features/normalize_uri.rb +17 -0
  19. data/lib/http/headers/known.rb +3 -0
  20. data/lib/http/options.rb +27 -21
  21. data/lib/http/redirector.rb +2 -1
  22. data/lib/http/request.rb +38 -30
  23. data/lib/http/request/body.rb +30 -1
  24. data/lib/http/request/writer.rb +21 -7
  25. data/lib/http/response.rb +7 -15
  26. data/lib/http/response/parser.rb +56 -16
  27. data/lib/http/timeout/global.rb +12 -14
  28. data/lib/http/timeout/per_operation.rb +5 -7
  29. data/lib/http/uri.rb +13 -0
  30. data/lib/http/version.rb +1 -1
  31. data/spec/lib/http/client_spec.rb +34 -7
  32. data/spec/lib/http/features/auto_inflate_spec.rb +38 -22
  33. data/spec/lib/http/features/instrumentation_spec.rb +56 -0
  34. data/spec/lib/http/features/logging_spec.rb +67 -0
  35. data/spec/lib/http/redirector_spec.rb +13 -0
  36. data/spec/lib/http/request/body_spec.rb +51 -0
  37. data/spec/lib/http/request/writer_spec.rb +20 -0
  38. data/spec/lib/http/request_spec.rb +6 -0
  39. data/spec/lib/http/response/parser_spec.rb +45 -0
  40. data/spec/lib/http/response_spec.rb +3 -4
  41. data/spec/lib/http_spec.rb +45 -65
  42. data/spec/regression_specs.rb +7 -0
  43. data/spec/support/dummy_server/servlet.rb +5 -0
  44. data/spec/support/http_handling_shared.rb +60 -64
  45. metadata +32 -21
  46. 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "3.3.0"
4
+ VERSION = "4.4.1"
5
5
  end
@@ -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
- url = "http://git.io/jNeY"
102
- client = HTTP.follow
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 = "https://github.com/httprb/http.rb/pull/197/ö無"
217
- client = HTTP.follow
218
- expect(client.get(url).to_s).to include "support for non-ascii URIs"
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 "stream_for" do
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 connection" do
19
- stream = subject.stream_for(connection, response)
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 => "not-supported"} }
28
+ let(:headers) { {:content_encoding => "identity"} }
26
29
 
27
- it "returns connection" do
28
- stream = subject.stream_for(connection, response)
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 connection" do
37
- stream = subject.stream_for(connection, response)
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::Inflater instance - connection wrapper" do
46
- stream = subject.stream_for(connection, response)
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::Inflater instance - connection wrapper" do
55
- stream = subject.stream_for(connection, response)
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::Inflater instance - connection wrapper" do
64
- stream = subject.stream_for(connection, response)
65
- expect(stream).to be_instance_of HTTP::Response::Inflater
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