http 5.0.0.pre → 5.0.0.pre2

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +17 -1
  3. data/.travis.yml +6 -4
  4. data/CHANGES.md +83 -0
  5. data/Gemfile +2 -1
  6. data/README.md +7 -6
  7. data/http.gemspec +11 -4
  8. data/lib/http/chainable.rb +8 -3
  9. data/lib/http/client.rb +32 -34
  10. data/lib/http/connection.rb +5 -5
  11. data/lib/http/content_type.rb +2 -2
  12. data/lib/http/feature.rb +3 -0
  13. data/lib/http/features/auto_deflate.rb +13 -7
  14. data/lib/http/features/auto_inflate.rb +6 -5
  15. data/lib/http/features/normalize_uri.rb +17 -0
  16. data/lib/http/headers.rb +48 -11
  17. data/lib/http/headers/known.rb +3 -0
  18. data/lib/http/mime_type/adapter.rb +1 -1
  19. data/lib/http/mime_type/json.rb +1 -0
  20. data/lib/http/options.rb +4 -7
  21. data/lib/http/redirector.rb +3 -1
  22. data/lib/http/request.rb +32 -29
  23. data/lib/http/request/body.rb +26 -1
  24. data/lib/http/request/writer.rb +3 -2
  25. data/lib/http/response.rb +17 -15
  26. data/lib/http/response/body.rb +1 -0
  27. data/lib/http/response/parser.rb +20 -6
  28. data/lib/http/response/status.rb +2 -1
  29. data/lib/http/timeout/global.rb +1 -3
  30. data/lib/http/timeout/per_operation.rb +1 -0
  31. data/lib/http/uri.rb +13 -0
  32. data/lib/http/version.rb +1 -1
  33. data/spec/lib/http/client_spec.rb +96 -14
  34. data/spec/lib/http/connection_spec.rb +8 -5
  35. data/spec/lib/http/features/auto_inflate_spec.rb +4 -2
  36. data/spec/lib/http/features/instrumentation_spec.rb +7 -6
  37. data/spec/lib/http/features/logging_spec.rb +6 -5
  38. data/spec/lib/http/headers_spec.rb +52 -17
  39. data/spec/lib/http/options/headers_spec.rb +1 -1
  40. data/spec/lib/http/options/merge_spec.rb +16 -16
  41. data/spec/lib/http/redirector_spec.rb +15 -1
  42. data/spec/lib/http/request/body_spec.rb +22 -0
  43. data/spec/lib/http/request/writer_spec.rb +13 -1
  44. data/spec/lib/http/request_spec.rb +5 -5
  45. data/spec/lib/http/response/parser_spec.rb +45 -0
  46. data/spec/lib/http/response/status_spec.rb +3 -3
  47. data/spec/lib/http/response_spec.rb +11 -22
  48. data/spec/lib/http_spec.rb +30 -1
  49. data/spec/support/black_hole.rb +1 -1
  50. data/spec/support/dummy_server.rb +6 -6
  51. data/spec/support/dummy_server/servlet.rb +8 -4
  52. data/spec/support/http_handling_shared.rb +4 -4
  53. data/spec/support/ssl_helper.rb +4 -4
  54. metadata +23 -16
@@ -108,12 +108,13 @@ module HTTP
108
108
  until data.empty?
109
109
  length = @socket.write(data)
110
110
  break unless data.bytesize > length
111
+
111
112
  data = data.byteslice(length..-1)
112
113
  end
113
114
  rescue Errno::EPIPE
114
115
  raise
115
- rescue IOError, SocketError, SystemCallError => ex
116
- raise ConnectionError, "error writing to socket: #{ex}", ex.backtrace
116
+ rescue IOError, SocketError, SystemCallError => e
117
+ raise ConnectionError, "error writing to socket: #{e}", e.backtrace
117
118
  end
118
119
  end
119
120
  end
@@ -7,7 +7,6 @@ require "http/content_type"
7
7
  require "http/mime_type"
8
8
  require "http/response/status"
9
9
  require "http/response/inflater"
10
- require "http/uri"
11
10
  require "http/cookie_jar"
12
11
  require "time"
13
12
 
@@ -26,8 +25,8 @@ module HTTP
26
25
  # @return [Body]
27
26
  attr_reader :body
28
27
 
29
- # @return [URI, nil]
30
- attr_reader :uri
28
+ # @return [Request]
29
+ attr_reader :request
31
30
 
32
31
  # @return [Hash]
33
32
  attr_reader :proxy_headers
@@ -41,10 +40,10 @@ module HTTP
41
40
  # @option opts [HTTP::Connection] :connection
42
41
  # @option opts [String] :encoding Encoding to use when reading body
43
42
  # @option opts [String] :body
44
- # @option opts [String] :uri
43
+ # @option opts [HTTP::Request] request
45
44
  def initialize(opts)
46
45
  @version = opts.fetch(:version)
47
- @uri = HTTP::URI.parse(opts.fetch(:uri)) if opts.include? :uri
46
+ @request = opts.fetch(:request)
48
47
  @status = HTTP::Response::Status.new(opts.fetch(:status))
49
48
  @headers = HTTP::Headers.coerce(opts[:headers] || {})
50
49
  @proxy_headers = HTTP::Headers.coerce(opts[:proxy_headers] || {})
@@ -61,24 +60,28 @@ module HTTP
61
60
 
62
61
  # @!method reason
63
62
  # @return (see HTTP::Response::Status#reason)
64
- def_delegator :status, :reason
63
+ def_delegator :@status, :reason
65
64
 
66
65
  # @!method code
67
66
  # @return (see HTTP::Response::Status#code)
68
- def_delegator :status, :code
67
+ def_delegator :@status, :code
69
68
 
70
69
  # @!method to_s
71
70
  # (see HTTP::Response::Body#to_s)
72
- def_delegator :body, :to_s
71
+ def_delegator :@body, :to_s
73
72
  alias to_str to_s
74
73
 
75
74
  # @!method readpartial
76
75
  # (see HTTP::Response::Body#readpartial)
77
- def_delegator :body, :readpartial
76
+ def_delegator :@body, :readpartial
78
77
 
79
78
  # @!method connection
80
79
  # (see HTTP::Response::Body#connection)
81
- def_delegator :body, :connection
80
+ def_delegator :@body, :connection
81
+
82
+ # @!method uri
83
+ # @return (see HTTP::Request#uri)
84
+ def_delegator :@request, :uri
82
85
 
83
86
  # Returns an Array ala Rack: `[status, headers, body]`
84
87
  #
@@ -150,12 +153,11 @@ module HTTP
150
153
 
151
154
  # Parse response body with corresponding MIME type adapter.
152
155
  #
153
- # @param [#to_s] as Parse as given MIME type
154
- # instead of the one determined from headers
155
- # @raise [HTTP::Error] if adapter not found
156
+ # @param type [#to_s] Parse as given MIME type.
157
+ # @raise (see MimeType.[])
156
158
  # @return [Object]
157
- def parse(as = nil)
158
- MimeType[as || mime_type].decode to_s
159
+ def parse(type)
160
+ MimeType[type].decode to_s
159
161
  end
160
162
 
161
163
  # Inspect a response
@@ -64,6 +64,7 @@ module HTTP
64
64
  # Assert that the body is actively being streamed
65
65
  def stream!
66
66
  raise StateError, "body has already been consumed" if @streaming == false
67
+
67
68
  @streaming = true
68
69
  end
69
70
 
@@ -49,14 +49,17 @@ module HTTP
49
49
  #
50
50
 
51
51
  def on_header_field(_response, field)
52
- @field = field
52
+ append_header if @reading_header_value
53
+ @field << field
53
54
  end
54
55
 
55
56
  def on_header_value(_response, value)
56
- @headers.add(@field, value) if @field
57
+ @reading_header_value = true
58
+ @field_value << value
57
59
  end
58
60
 
59
61
  def on_headers_complete(_reposse)
62
+ append_header if @reading_header_value
60
63
  @finished[:headers] = true
61
64
  end
62
65
 
@@ -89,15 +92,26 @@ module HTTP
89
92
  def reset
90
93
  @state.reset!
91
94
 
92
- @finished = Hash.new(false)
93
- @headers = HTTP::Headers.new
94
- @field = nil
95
- @chunk = nil
95
+ @finished = Hash.new(false)
96
+ @headers = HTTP::Headers.new
97
+ @reading_header_value = false
98
+ @field = +""
99
+ @field_value = +""
100
+ @chunk = nil
96
101
  end
97
102
 
98
103
  def finished?
99
104
  @finished[:message]
100
105
  end
106
+
107
+ private
108
+
109
+ def append_header
110
+ @headers.add(@field, @field_value)
111
+ @reading_header_value = false
112
+ @field_value = +""
113
+ @field = +""
114
+ end
101
115
  end
102
116
  end
103
117
  end
@@ -132,7 +132,7 @@ module HTTP
132
132
  end
133
133
 
134
134
  SYMBOLS.each do |code, symbol|
135
- class_eval <<-RUBY, __FILE__, __LINE__
135
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
136
136
  def #{symbol}? # def bad_request?
137
137
  #{code} == code # 400 == code
138
138
  end # end
@@ -141,6 +141,7 @@ module HTTP
141
141
 
142
142
  def __setobj__(obj)
143
143
  raise TypeError, "Expected #{obj.inspect} to respond to #to_i" unless obj.respond_to? :to_i
144
+
144
145
  @code = obj.to_i
145
146
  end
146
147
 
@@ -121,9 +121,7 @@ module HTTP
121
121
 
122
122
  def log_time
123
123
  @time_left -= (Time.now - @started)
124
- if @time_left <= 0
125
- raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds"
126
- end
124
+ raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds" if @time_left <= 0
127
125
 
128
126
  reset_timer
129
127
  end
@@ -66,6 +66,7 @@ module HTTP
66
66
  return result if result != :wait_readable
67
67
 
68
68
  raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
69
+
69
70
  # marking the socket for timeout. Why is this not being raised immediately?
70
71
  # it seems there is some race-condition on the network level between calling
71
72
  # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "5.0.0.pre"
4
+ VERSION = "5.0.0.pre2"
5
5
  end
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # coding: utf-8
2
+ # frozen_string_literal: true
3
3
 
4
4
  require "support/http_handling_shared"
5
5
  require "support/dummy_server"
@@ -10,7 +10,8 @@ RSpec.describe HTTP::Client do
10
10
 
11
11
  StubbedClient = Class.new(HTTP::Client) do
12
12
  def perform(request, options)
13
- stubs.fetch(request.uri) { super(request, options) }
13
+ stubbed = stubs[request.uri]
14
+ stubbed ? stubbed.call(request) : super(request, options)
14
15
  end
15
16
 
16
17
  def stubs
@@ -27,20 +28,26 @@ RSpec.describe HTTP::Client do
27
28
  end
28
29
 
29
30
  def redirect_response(location, status = 302)
30
- HTTP::Response.new(
31
- :status => status,
32
- :version => "1.1",
33
- :headers => {"Location" => location},
34
- :body => ""
35
- )
31
+ lambda do |request|
32
+ HTTP::Response.new(
33
+ :status => status,
34
+ :version => "1.1",
35
+ :headers => {"Location" => location},
36
+ :body => "",
37
+ :request => request
38
+ )
39
+ end
36
40
  end
37
41
 
38
42
  def simple_response(body, status = 200)
39
- HTTP::Response.new(
40
- :status => status,
41
- :version => "1.1",
42
- :body => body
43
- )
43
+ lambda do |request|
44
+ HTTP::Response.new(
45
+ :status => status,
46
+ :version => "1.1",
47
+ :body => body,
48
+ :request => request
49
+ )
50
+ end
44
51
  end
45
52
 
46
53
  describe "following redirects" do
@@ -234,7 +241,7 @@ RSpec.describe HTTP::Client do
234
241
 
235
242
  context "when :auto_deflate was specified" do
236
243
  let(:headers) { {"Content-Length" => "12"} }
237
- let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}} }
244
+ let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}}, :body => "foo" }
238
245
 
239
246
  it "deletes Content-Length header" do
240
247
  expect(client).to receive(:perform) do |req, _|
@@ -251,6 +258,73 @@ RSpec.describe HTTP::Client do
251
258
 
252
259
  client.request(:get, "http://example.com/")
253
260
  end
261
+
262
+ context "and there is no body" do
263
+ let(:client) { described_class.new :headers => headers, :features => {:auto_deflate => {}} }
264
+
265
+ it "doesn't set Content-Encoding header" do
266
+ expect(client).to receive(:perform) do |req, _|
267
+ expect(req.headers).not_to include "Content-Encoding"
268
+ end
269
+
270
+ client.request(:get, "http://example.com/")
271
+ end
272
+ end
273
+ end
274
+
275
+ context "Feature" do
276
+ let(:feature_class) do
277
+ Class.new(HTTP::Feature) do
278
+ attr_reader :captured_request, :captured_response, :captured_error
279
+
280
+ def wrap_request(request)
281
+ @captured_request = request
282
+ end
283
+
284
+ def wrap_response(response)
285
+ @captured_response = response
286
+ end
287
+
288
+ def on_error(request, error)
289
+ @captured_request = request
290
+ @captured_error = error
291
+ end
292
+ end
293
+ end
294
+ it "is given a chance to wrap the Request" do
295
+ feature_instance = feature_class.new
296
+
297
+ response = client.use(:test_feature => feature_instance).
298
+ request(:get, dummy.endpoint)
299
+
300
+ expect(response.code).to eq(200)
301
+ expect(feature_instance.captured_request.verb).to eq(:get)
302
+ expect(feature_instance.captured_request.uri.to_s).to eq(dummy.endpoint + "/")
303
+ end
304
+
305
+ it "is given a chance to wrap the Response" do
306
+ feature_instance = feature_class.new
307
+
308
+ response = client.use(:test_feature => feature_instance).
309
+ request(:get, dummy.endpoint)
310
+
311
+ expect(feature_instance.captured_response).to eq(response)
312
+ end
313
+
314
+ it "is given a chance to handle an error" do
315
+ sleep_url = "#{dummy.endpoint}/sleep"
316
+ feature_instance = feature_class.new
317
+
318
+ expect do
319
+ client.use(:test_feature => feature_instance).
320
+ timeout(0.2).
321
+ request(:post, sleep_url)
322
+ end.to raise_error(HTTP::TimeoutError)
323
+
324
+ expect(feature_instance.captured_error).to be_a(HTTP::TimeoutError)
325
+ expect(feature_instance.captured_request.verb).to eq(:post)
326
+ expect(feature_instance.captured_request.uri.to_s).to eq(sleep_url)
327
+ end
254
328
  end
255
329
  end
256
330
 
@@ -304,6 +378,14 @@ RSpec.describe HTTP::Client do
304
378
  client.get(dummy.endpoint).to_s
305
379
  end
306
380
 
381
+ it "provides access to the Request from the Response" do
382
+ unique_value = "20190424"
383
+ response = client.headers("X-Value" => unique_value).get(dummy.endpoint)
384
+
385
+ expect(response.request).to be_a(HTTP::Request)
386
+ expect(response.request.headers["X-Value"]).to eq(unique_value)
387
+ end
388
+
307
389
  context "with HEAD request" do
308
390
  it "does not iterates through body" do
309
391
  expect_any_instance_of(HTTP::Connection).to_not receive(:readpartial)
@@ -3,9 +3,9 @@
3
3
  RSpec.describe HTTP::Connection do
4
4
  let(:req) do
5
5
  HTTP::Request.new(
6
- :verb => :get,
7
- :uri => "http://example.com/",
8
- :headers => {}
6
+ :verb => :get,
7
+ :uri => "http://example.com/",
8
+ :headers => {}
9
9
  )
10
10
  end
11
11
  let(:socket) { double(:connect => nil) }
@@ -20,14 +20,17 @@ RSpec.describe HTTP::Connection do
20
20
  <<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n")
21
21
  | HTTP/1.1 200 OK
22
22
  | Content-Type: text
23
+ | foo_bar: 123
23
24
  |
24
25
  RESPONSE
25
26
  end
26
27
  end
27
28
 
28
- it "reads data in parts" do
29
+ it "populates headers collection, preserving casing" do
29
30
  connection.read_headers!
30
- expect(connection.headers).to eq("Content-Type" => "text")
31
+ expect(connection.headers).to eq("Content-Type" => "text", "foo_bar" => "123")
32
+ expect(connection.headers["Foo-Bar"]).to eq("123")
33
+ expect(connection.headers["foo_bar"]).to eq("123")
31
34
  end
32
35
  end
33
36
 
@@ -11,7 +11,8 @@ RSpec.describe HTTP::Features::AutoInflate do
11
11
  :version => "1.1",
12
12
  :status => 200,
13
13
  :headers => headers,
14
- :connection => connection
14
+ :connection => connection,
15
+ :request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
15
16
  )
16
17
  end
17
18
 
@@ -73,7 +74,8 @@ RSpec.describe HTTP::Features::AutoInflate do
73
74
  :status => 200,
74
75
  :headers => {:content_encoding => "gzip"},
75
76
  :connection => connection,
76
- :uri => "https://example.com"
77
+ :uri => "https://example.com",
78
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
77
79
  )
78
80
  end
79
81
 
@@ -7,10 +7,10 @@ RSpec.describe HTTP::Features::Instrumentation do
7
7
  describe "logging the request" do
8
8
  let(:request) do
9
9
  HTTP::Request.new(
10
- :verb => :post,
11
- :uri => "https://example.com/",
10
+ :verb => :post,
11
+ :uri => "https://example.com/",
12
12
  :headers => {:accept => "application/json"},
13
- :body => '{"hello": "world!"}'
13
+ :body => '{"hello": "world!"}'
14
14
  )
15
15
  end
16
16
 
@@ -25,10 +25,11 @@ RSpec.describe HTTP::Features::Instrumentation do
25
25
  let(:response) do
26
26
  HTTP::Response.new(
27
27
  :version => "1.1",
28
- :uri => "https://example.com",
29
- :status => 200,
28
+ :uri => "https://example.com",
29
+ :status => 200,
30
30
  :headers => {:content_type => "application/json"},
31
- :body => '{"success": true}'
31
+ :body => '{"success": true}',
32
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
32
33
  )
33
34
  end
34
35