http 5.0.0.pre → 5.0.0.pre2

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