http 4.4.1 → 5.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +85 -0
  3. data/.gitignore +6 -10
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/style.yml +32 -0
  7. data/.rubocop.yml +8 -110
  8. data/.rubocop_todo.yml +206 -0
  9. data/.yardopts +1 -1
  10. data/CHANGES.md +200 -3
  11. data/Gemfile +18 -10
  12. data/LICENSE.txt +1 -1
  13. data/README.md +48 -86
  14. data/Rakefile +2 -10
  15. data/SECURITY.md +5 -0
  16. data/http.gemspec +9 -8
  17. data/lib/http/chainable.rb +23 -17
  18. data/lib/http/client.rb +44 -34
  19. data/lib/http/connection.rb +11 -7
  20. data/lib/http/content_type.rb +12 -7
  21. data/lib/http/errors.rb +3 -0
  22. data/lib/http/feature.rb +3 -1
  23. data/lib/http/features/auto_deflate.rb +6 -6
  24. data/lib/http/features/auto_inflate.rb +6 -7
  25. data/lib/http/features/instrumentation.rb +1 -1
  26. data/lib/http/features/logging.rb +19 -21
  27. data/lib/http/headers.rb +50 -13
  28. data/lib/http/mime_type/adapter.rb +3 -1
  29. data/lib/http/mime_type/json.rb +1 -0
  30. data/lib/http/options.rb +5 -8
  31. data/lib/http/redirector.rb +55 -4
  32. data/lib/http/request/body.rb +1 -0
  33. data/lib/http/request/writer.rb +9 -4
  34. data/lib/http/request.rb +28 -11
  35. data/lib/http/response/body.rb +6 -4
  36. data/lib/http/response/inflater.rb +1 -1
  37. data/lib/http/response/parser.rb +74 -62
  38. data/lib/http/response/status.rb +4 -3
  39. data/lib/http/response.rb +44 -18
  40. data/lib/http/timeout/global.rb +20 -36
  41. data/lib/http/timeout/null.rb +2 -1
  42. data/lib/http/timeout/per_operation.rb +32 -55
  43. data/lib/http/uri.rb +51 -6
  44. data/lib/http/version.rb +1 -1
  45. data/spec/lib/http/client_spec.rb +155 -30
  46. data/spec/lib/http/connection_spec.rb +8 -5
  47. data/spec/lib/http/features/auto_inflate_spec.rb +3 -2
  48. data/spec/lib/http/features/instrumentation_spec.rb +27 -21
  49. data/spec/lib/http/features/logging_spec.rb +8 -10
  50. data/spec/lib/http/headers_spec.rb +53 -18
  51. data/spec/lib/http/options/headers_spec.rb +1 -1
  52. data/spec/lib/http/options/merge_spec.rb +16 -16
  53. data/spec/lib/http/redirector_spec.rb +133 -3
  54. data/spec/lib/http/request/body_spec.rb +3 -3
  55. data/spec/lib/http/request/writer_spec.rb +25 -2
  56. data/spec/lib/http/request_spec.rb +5 -5
  57. data/spec/lib/http/response/body_spec.rb +5 -5
  58. data/spec/lib/http/response/parser_spec.rb +33 -4
  59. data/spec/lib/http/response/status_spec.rb +3 -3
  60. data/spec/lib/http/response_spec.rb +80 -3
  61. data/spec/lib/http/uri_spec.rb +39 -0
  62. data/spec/lib/http_spec.rb +30 -3
  63. data/spec/spec_helper.rb +21 -21
  64. data/spec/support/black_hole.rb +1 -1
  65. data/spec/support/dummy_server/servlet.rb +19 -6
  66. data/spec/support/dummy_server.rb +7 -7
  67. data/spec/support/fuubar.rb +21 -0
  68. data/spec/support/http_handling_shared.rb +5 -5
  69. data/spec/support/simplecov.rb +19 -0
  70. data/spec/support/ssl_helper.rb +4 -4
  71. metadata +22 -14
  72. data/.coveralls.yml +0 -1
  73. data/.travis.yml +0 -39
@@ -21,7 +21,7 @@ module HTTP
21
21
 
22
22
  def connect(socket_class, host, port, nodelay = false)
23
23
  reset_timer
24
- ::Timeout.timeout(@time_left, TimeoutError) do
24
+ ::Timeout.timeout(@time_left, ConnectTimeoutError) do
25
25
  @socket = socket_class.open(host, port)
26
26
  @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
27
27
  end
@@ -35,12 +35,10 @@ module HTTP
35
35
  begin
36
36
  @socket.connect_nonblock
37
37
  rescue IO::WaitReadable
38
- IO.select([@socket], nil, nil, @time_left)
39
- log_time
38
+ wait_readable_or_timeout
40
39
  retry
41
40
  rescue IO::WaitWritable
42
- IO.select(nil, [@socket], nil, @time_left)
43
- log_time
41
+ wait_writable_or_timeout
44
42
  retry
45
43
  end
46
44
  end
@@ -59,22 +57,12 @@ module HTTP
59
57
 
60
58
  private
61
59
 
62
- if RUBY_VERSION < "2.1.0"
63
- def read_nonblock(size, buffer = nil)
64
- @socket.read_nonblock(size, buffer)
65
- end
66
-
67
- def write_nonblock(data)
68
- @socket.write_nonblock(data)
69
- end
70
- else
71
- def read_nonblock(size, buffer = nil)
72
- @socket.read_nonblock(size, buffer, :exception => false)
73
- end
60
+ def read_nonblock(size, buffer = nil)
61
+ @socket.read_nonblock(size, buffer, :exception => false)
62
+ end
74
63
 
75
- def write_nonblock(data)
76
- @socket.write_nonblock(data, :exception => false)
77
- end
64
+ def write_nonblock(data)
65
+ @socket.write_nonblock(data, :exception => false)
78
66
  end
79
67
 
80
68
  # Perform the given I/O operation with the given argument
@@ -82,20 +70,18 @@ module HTTP
82
70
  reset_timer
83
71
 
84
72
  loop do
85
- begin
86
- result = yield
87
-
88
- case result
89
- when :wait_readable then wait_readable_or_timeout
90
- when :wait_writable then wait_writable_or_timeout
91
- when NilClass then return :eof
92
- else return result
93
- end
94
- rescue IO::WaitReadable
95
- wait_readable_or_timeout
96
- rescue IO::WaitWritable
97
- wait_writable_or_timeout
73
+ result = yield
74
+
75
+ case result
76
+ when :wait_readable then wait_readable_or_timeout
77
+ when :wait_writable then wait_writable_or_timeout
78
+ when NilClass then return :eof
79
+ else return result
98
80
  end
81
+ rescue IO::WaitReadable
82
+ wait_readable_or_timeout
83
+ rescue IO::WaitWritable
84
+ wait_writable_or_timeout
99
85
  end
100
86
  rescue EOFError
101
87
  :eof
@@ -121,9 +107,7 @@ module HTTP
121
107
 
122
108
  def log_time
123
109
  @time_left -= (Time.now - @started)
124
- if @time_left <= 0
125
- raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds"
126
- end
110
+ raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds" if @time_left <= 0
127
111
 
128
112
  reset_timer
129
113
  end
@@ -12,7 +12,7 @@ module HTTP
12
12
 
13
13
  attr_reader :options, :socket
14
14
 
15
- def initialize(options = {}) # rubocop:disable Style/OptionHash
15
+ def initialize(options = {})
16
16
  @options = options
17
17
  end
18
18
 
@@ -36,6 +36,7 @@ module HTTP
36
36
  connect_ssl
37
37
 
38
38
  return unless ssl_context.verify_mode == OpenSSL::SSL::VERIFY_PEER
39
+ return if ssl_context.respond_to?(:verify_hostname) && !ssl_context.verify_hostname
39
40
 
40
41
  @socket.post_connection_check(host)
41
42
  end
@@ -20,7 +20,7 @@ module HTTP
20
20
  end
21
21
 
22
22
  def connect(socket_class, host, port, nodelay = false)
23
- ::Timeout.timeout(@connect_timeout, TimeoutError) do
23
+ ::Timeout.timeout(@connect_timeout, ConnectTimeoutError) do
24
24
  @socket = socket_class.open(host, port)
25
25
  @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
26
26
  end
@@ -34,65 +34,42 @@ module HTTP
34
34
  end
35
35
  end
36
36
 
37
- # NIO with exceptions
38
- if RUBY_VERSION < "2.1.0"
39
- # Read data from the socket
40
- def readpartial(size, buffer = nil)
41
- rescue_readable do
42
- @socket.read_nonblock(size, buffer)
43
- end
44
- rescue EOFError
45
- :eof
46
- end
47
-
48
- # Write data to the socket
49
- def write(data)
50
- rescue_writable do
51
- @socket.write_nonblock(data)
52
- end
53
- rescue EOFError
54
- :eof
55
- end
56
-
57
- # NIO without exceptions
58
- else
59
- # Read data from the socket
60
- def readpartial(size, buffer = nil)
61
- timeout = false
62
- loop do
63
- result = @socket.read_nonblock(size, buffer, :exception => false)
64
-
65
- return :eof if result.nil?
66
- return result if result != :wait_readable
67
-
68
- raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
69
- # marking the socket for timeout. Why is this not being raised immediately?
70
- # it seems there is some race-condition on the network level between calling
71
- # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
72
- # for reads, and when waiting for x seconds, it returns nil suddenly without completing
73
- # the x seconds. In a normal case this would be a timeout on wait/read, but it can
74
- # also mean that the socket has been closed by the server. Therefore we "mark" the
75
- # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
76
- # timeout. Else, the first timeout was a proper timeout.
77
- # This hack has to be done because io/wait#wait_readable doesn't provide a value for when
78
- # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
79
- timeout = true unless @socket.to_io.wait_readable(@read_timeout)
80
- end
37
+ # Read data from the socket
38
+ def readpartial(size, buffer = nil)
39
+ timeout = false
40
+ loop do
41
+ result = @socket.read_nonblock(size, buffer, :exception => false)
42
+
43
+ return :eof if result.nil?
44
+ return result if result != :wait_readable
45
+
46
+ raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
47
+
48
+ # marking the socket for timeout. Why is this not being raised immediately?
49
+ # it seems there is some race-condition on the network level between calling
50
+ # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
51
+ # for reads, and when waiting for x seconds, it returns nil suddenly without completing
52
+ # the x seconds. In a normal case this would be a timeout on wait/read, but it can
53
+ # also mean that the socket has been closed by the server. Therefore we "mark" the
54
+ # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
55
+ # timeout. Else, the first timeout was a proper timeout.
56
+ # This hack has to be done because io/wait#wait_readable doesn't provide a value for when
57
+ # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
58
+ timeout = true unless @socket.to_io.wait_readable(@read_timeout)
81
59
  end
60
+ end
82
61
 
83
- # Write data to the socket
84
- def write(data)
85
- timeout = false
86
- loop do
87
- result = @socket.write_nonblock(data, :exception => false)
88
- return result unless result == :wait_writable
62
+ # Write data to the socket
63
+ def write(data)
64
+ timeout = false
65
+ loop do
66
+ result = @socket.write_nonblock(data, :exception => false)
67
+ return result unless result == :wait_writable
89
68
 
90
- raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
69
+ raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
91
70
 
92
- timeout = true unless @socket.to_io.wait_writable(@write_timeout)
93
- end
71
+ timeout = true unless @socket.to_io.wait_writable(@write_timeout)
94
72
  end
95
-
96
73
  end
97
74
  end
98
75
  end
data/lib/http/uri.rb CHANGED
@@ -9,7 +9,6 @@ module HTTP
9
9
  def_delegators :@uri, :scheme, :normalized_scheme, :scheme=
10
10
  def_delegators :@uri, :user, :normalized_user, :user=
11
11
  def_delegators :@uri, :password, :normalized_password, :password=
12
- def_delegators :@uri, :host, :normalized_host, :host=
13
12
  def_delegators :@uri, :authority, :normalized_authority, :authority=
14
13
  def_delegators :@uri, :origin, :origin=
15
14
  def_delegators :@uri, :normalized_port, :port=
@@ -20,6 +19,18 @@ module HTTP
20
19
  def_delegators :@uri, :fragment, :normalized_fragment, :fragment=
21
20
  def_delegators :@uri, :omit, :join, :normalize
22
21
 
22
+ # Host, either a domain name or IP address. If the host is an IPv6 address, it will be returned
23
+ # without brackets surrounding it.
24
+ #
25
+ # @return [String] The host of the URI
26
+ attr_reader :host
27
+
28
+ # Normalized host, either a domain name or IP address. If the host is an IPv6 address, it will
29
+ # be returned without brackets surrounding it.
30
+ #
31
+ # @return [String] The normalized host of the URI
32
+ attr_reader :normalized_host
33
+
23
34
  # @private
24
35
  HTTP_SCHEME = "http"
25
36
 
@@ -31,11 +42,11 @@ module HTTP
31
42
  uri = HTTP::URI.parse uri
32
43
 
33
44
  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
45
+ :scheme => uri.normalized_scheme,
46
+ :authority => uri.normalized_authority,
47
+ :path => uri.normalized_path,
48
+ :query => uri.query,
49
+ :fragment => uri.normalized_fragment
39
50
  )
40
51
  end
41
52
 
@@ -83,6 +94,9 @@ module HTTP
83
94
  else
84
95
  raise TypeError, "expected Hash for options, got #{options_or_uri.class}"
85
96
  end
97
+
98
+ @host = process_ipv6_brackets(@uri.host)
99
+ @normalized_host = process_ipv6_brackets(@uri.normalized_host)
86
100
  end
87
101
 
88
102
  # Are these URI objects equal? Normalizes both URIs prior to comparison
@@ -110,6 +124,17 @@ module HTTP
110
124
  @hash ||= to_s.hash * -1
111
125
  end
112
126
 
127
+ # Sets the host component for the URI.
128
+ #
129
+ # @param [String, #to_str] new_host The new host component.
130
+ # @return [void]
131
+ def host=(new_host)
132
+ @uri.host = process_ipv6_brackets(new_host, :brackets => true)
133
+
134
+ @host = process_ipv6_brackets(@uri.host)
135
+ @normalized_host = process_ipv6_brackets(@uri.normalized_host)
136
+ end
137
+
113
138
  # Port number, either as specified or the default if unspecified
114
139
  #
115
140
  # @return [Integer] port number
@@ -146,5 +171,25 @@ module HTTP
146
171
  def inspect
147
172
  format("#<%s:0x%014x URI:%s>", self.class.name, object_id << 1, to_s)
148
173
  end
174
+
175
+ private
176
+
177
+ # Process a URI host, adding or removing surrounding brackets if the host is an IPv6 address.
178
+ #
179
+ # @param [Boolean] brackets When true, brackets will be added to IPv6 addresses if missing. When
180
+ # false, they will be removed if present.
181
+ #
182
+ # @return [String] Host with IPv6 address brackets added or removed
183
+ def process_ipv6_brackets(raw_host, brackets: false)
184
+ ip = IPAddr.new(raw_host)
185
+
186
+ if ip.ipv6?
187
+ brackets ? "[#{ip}]" : ip.to_s
188
+ else
189
+ raw_host
190
+ end
191
+ rescue IPAddr::Error
192
+ raw_host
193
+ end
149
194
  end
150
195
  end
data/lib/http/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "4.4.1"
4
+ VERSION = "5.1.1"
5
5
  end
@@ -1,5 +1,8 @@
1
- # frozen_string_literal: true
2
1
  # coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require "cgi"
5
+ require "logger"
3
6
 
4
7
  require "support/http_handling_shared"
5
8
  require "support/dummy_server"
@@ -8,39 +11,50 @@ require "support/ssl_helper"
8
11
  RSpec.describe HTTP::Client do
9
12
  run_server(:dummy) { DummyServer.new }
10
13
 
11
- StubbedClient = Class.new(HTTP::Client) do
12
- def perform(request, options)
13
- stubs.fetch(request.uri) { super(request, options) }
14
- end
14
+ before do
15
+ stubbed_client = Class.new(HTTP::Client) do
16
+ def perform(request, options)
17
+ stubbed = stubs[HTTP::URI::NORMALIZER.call(request.uri).to_s]
18
+ stubbed ? stubbed.call(request) : super(request, options)
19
+ end
15
20
 
16
- def stubs
17
- @stubs ||= {}
18
- end
21
+ def stubs
22
+ @stubs ||= {}
23
+ end
19
24
 
20
- def stub(stubs)
21
- @stubs = stubs.each_with_object({}) do |(k, v), o|
22
- o[HTTP::URI.parse k] = v
25
+ def stub(stubs)
26
+ @stubs = stubs.transform_keys do |k|
27
+ HTTP::URI::NORMALIZER.call(k).to_s
28
+ end
29
+
30
+ self
23
31
  end
32
+ end
24
33
 
25
- self
34
+ def redirect_response(location, status = 302)
35
+ lambda do |request|
36
+ HTTP::Response.new(
37
+ :status => status,
38
+ :version => "1.1",
39
+ :headers => {"Location" => location},
40
+ :body => "",
41
+ :request => request
42
+ )
43
+ end
26
44
  end
27
- end
28
45
 
29
- def redirect_response(location, status = 302)
30
- HTTP::Response.new(
31
- :status => status,
32
- :version => "1.1",
33
- :headers => {"Location" => location},
34
- :body => ""
35
- )
36
- end
46
+ def simple_response(body, status = 200)
47
+ lambda do |request|
48
+ HTTP::Response.new(
49
+ :status => status,
50
+ :version => "1.1",
51
+ :body => body,
52
+ :request => request
53
+ )
54
+ end
55
+ end
37
56
 
38
- def simple_response(body, status = 200)
39
- HTTP::Response.new(
40
- :status => status,
41
- :version => "1.1",
42
- :body => body
43
- )
57
+ stub_const("StubbedClient", stubbed_client)
44
58
  end
45
59
 
46
60
  describe "following redirects" do
@@ -104,6 +118,39 @@ RSpec.describe HTTP::Client do
104
118
  end
105
119
  end
106
120
 
121
+ describe "following redirects with logging" do
122
+ let(:logger) do
123
+ logger = Logger.new(logdev)
124
+ logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
125
+ logger.level = Logger::INFO
126
+ logger
127
+ end
128
+
129
+ let(:logdev) { StringIO.new }
130
+
131
+ it "logs all requests" do
132
+ client = StubbedClient.new(:follow => true, :features => { :logging => { :logger => logger } }).stub(
133
+ "http://example.com/" => redirect_response("/1"),
134
+ "http://example.com/1" => redirect_response("/2"),
135
+ "http://example.com/2" => redirect_response("/3"),
136
+ "http://example.com/3" => simple_response("OK")
137
+ )
138
+
139
+ expect { client.get("http://example.com/") }.not_to raise_error
140
+
141
+ expect(logdev.string).to eq <<~OUTPUT
142
+ ** INFO **
143
+ > GET http://example.com/
144
+ ** INFO **
145
+ > GET http://example.com/1
146
+ ** INFO **
147
+ > GET http://example.com/2
148
+ ** INFO **
149
+ > GET http://example.com/3
150
+ OUTPUT
151
+ end
152
+ end
153
+
107
154
  describe "parsing params" do
108
155
  let(:client) { HTTP::Client.new }
109
156
  before { allow(client).to receive :perform }
@@ -193,7 +240,7 @@ RSpec.describe HTTP::Client do
193
240
  context "when passing an HTTP::FormData object directly" do
194
241
  it "creates url encoded form data object" do
195
242
  client = HTTP::Client.new
196
- form_data = HTTP::FormData::Multipart.new(:foo => "bar")
243
+ form_data = HTTP::FormData::Multipart.new({ :foo => "bar" })
197
244
 
198
245
  allow(client).to receive(:perform)
199
246
 
@@ -279,6 +326,75 @@ RSpec.describe HTTP::Client do
279
326
  end
280
327
  end
281
328
  end
329
+
330
+ context "Feature" do
331
+ let(:feature_class) do
332
+ Class.new(HTTP::Feature) do
333
+ attr_reader :captured_request, :captured_response, :captured_error
334
+
335
+ def wrap_request(request)
336
+ @captured_request = request
337
+ end
338
+
339
+ def wrap_response(response)
340
+ @captured_response = response
341
+ end
342
+
343
+ def on_error(request, error)
344
+ @captured_request = request
345
+ @captured_error = error
346
+ end
347
+ end
348
+ end
349
+
350
+ it "is given a chance to wrap the Request" do
351
+ feature_instance = feature_class.new
352
+
353
+ response = client.use(:test_feature => feature_instance).
354
+ request(:get, dummy.endpoint)
355
+
356
+ expect(response.code).to eq(200)
357
+ expect(feature_instance.captured_request.verb).to eq(:get)
358
+ expect(feature_instance.captured_request.uri.to_s).to eq("#{dummy.endpoint}/")
359
+ end
360
+
361
+ it "is given a chance to wrap the Response" do
362
+ feature_instance = feature_class.new
363
+
364
+ response = client.use(:test_feature => feature_instance).
365
+ request(:get, dummy.endpoint)
366
+
367
+ expect(feature_instance.captured_response).to eq(response)
368
+ end
369
+
370
+ it "is given a chance to handle an error" do
371
+ sleep_url = "#{dummy.endpoint}/sleep"
372
+ feature_instance = feature_class.new
373
+
374
+ expect do
375
+ client.use(:test_feature => feature_instance).
376
+ timeout(0.2).
377
+ request(:post, sleep_url)
378
+ end.to raise_error(HTTP::TimeoutError)
379
+
380
+ expect(feature_instance.captured_error).to be_a(HTTP::TimeoutError)
381
+ expect(feature_instance.captured_request.verb).to eq(:post)
382
+ expect(feature_instance.captured_request.uri.to_s).to eq(sleep_url)
383
+ end
384
+
385
+ it "is given a chance to handle a connection timeout error" do
386
+ allow(TCPSocket).to receive(:open) { sleep 1 }
387
+ sleep_url = "#{dummy.endpoint}/sleep"
388
+ feature_instance = feature_class.new
389
+
390
+ expect do
391
+ client.use(:test_feature => feature_instance).
392
+ timeout(0.001).
393
+ request(:post, sleep_url)
394
+ end.to raise_error(HTTP::ConnectTimeoutError)
395
+ expect(feature_instance.captured_error).to be_a(HTTP::ConnectTimeoutError)
396
+ end
397
+ end
282
398
  end
283
399
 
284
400
  include_context "HTTP handling" do
@@ -288,7 +404,8 @@ RSpec.describe HTTP::Client do
288
404
  let(:client) { described_class.new(options.merge(extra_options)) }
289
405
  end
290
406
 
291
- describe "working with SSL" do
407
+ # TODO: https://github.com/httprb/http/issues/627
408
+ xdescribe "working with SSL" do
292
409
  run_server(:dummy_ssl) { DummyServer.new(:ssl => true) }
293
410
 
294
411
  let(:extra_options) { {} }
@@ -331,6 +448,14 @@ RSpec.describe HTTP::Client do
331
448
  client.get(dummy.endpoint).to_s
332
449
  end
333
450
 
451
+ it "provides access to the Request from the Response" do
452
+ unique_value = "20190424"
453
+ response = client.headers("X-Value" => unique_value).get(dummy.endpoint)
454
+
455
+ expect(response.request).to be_a(HTTP::Request)
456
+ expect(response.request.headers["X-Value"]).to eq(unique_value)
457
+ end
458
+
334
459
  context "with HEAD request" do
335
460
  it "does not iterates through body" do
336
461
  expect_any_instance_of(HTTP::Connection).to_not receive(:readpartial)
@@ -421,7 +546,7 @@ RSpec.describe HTTP::Client do
421
546
  BODY
422
547
  end
423
548
 
424
- it "raises HTTP::ConnectionError" do
549
+ xit "raises HTTP::ConnectionError" do
425
550
  expect { client.get(dummy.endpoint).to_s }.to raise_error(HTTP::ConnectionError)
426
551
  end
427
552
  end
@@ -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,7 @@ 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
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
77
78
  )
78
79
  end
79
80