http 4.4.1 → 5.1.1

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 (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