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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +85 -0
- data/.gitignore +6 -10
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +8 -110
- data/.rubocop_todo.yml +206 -0
- data/.yardopts +1 -1
- data/CHANGES.md +200 -3
- data/Gemfile +18 -10
- data/LICENSE.txt +1 -1
- data/README.md +48 -86
- data/Rakefile +2 -10
- data/SECURITY.md +5 -0
- data/http.gemspec +9 -8
- data/lib/http/chainable.rb +23 -17
- data/lib/http/client.rb +44 -34
- data/lib/http/connection.rb +11 -7
- data/lib/http/content_type.rb +12 -7
- data/lib/http/errors.rb +3 -0
- data/lib/http/feature.rb +3 -1
- data/lib/http/features/auto_deflate.rb +6 -6
- data/lib/http/features/auto_inflate.rb +6 -7
- data/lib/http/features/instrumentation.rb +1 -1
- data/lib/http/features/logging.rb +19 -21
- data/lib/http/headers.rb +50 -13
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +5 -8
- data/lib/http/redirector.rb +55 -4
- data/lib/http/request/body.rb +1 -0
- data/lib/http/request/writer.rb +9 -4
- data/lib/http/request.rb +28 -11
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +74 -62
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +44 -18
- data/lib/http/timeout/global.rb +20 -36
- data/lib/http/timeout/null.rb +2 -1
- data/lib/http/timeout/per_operation.rb +32 -55
- data/lib/http/uri.rb +51 -6
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +155 -30
- data/spec/lib/http/connection_spec.rb +8 -5
- data/spec/lib/http/features/auto_inflate_spec.rb +3 -2
- data/spec/lib/http/features/instrumentation_spec.rb +27 -21
- data/spec/lib/http/features/logging_spec.rb +8 -10
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +1 -1
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +133 -3
- data/spec/lib/http/request/body_spec.rb +3 -3
- data/spec/lib/http/request/writer_spec.rb +25 -2
- data/spec/lib/http/request_spec.rb +5 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +33 -4
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +80 -3
- data/spec/lib/http/uri_spec.rb +39 -0
- data/spec/lib/http_spec.rb +30 -3
- data/spec/spec_helper.rb +21 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +19 -6
- data/spec/support/dummy_server.rb +7 -7
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +5 -5
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +22 -14
- data/.coveralls.yml +0 -1
- data/.travis.yml +0 -39
data/lib/http/timeout/global.rb
CHANGED
@@ -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,
|
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
|
-
|
39
|
-
log_time
|
38
|
+
wait_readable_or_timeout
|
40
39
|
retry
|
41
40
|
rescue IO::WaitWritable
|
42
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
76
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
data/lib/http/timeout/null.rb
CHANGED
@@ -12,7 +12,7 @@ module HTTP
|
|
12
12
|
|
13
13
|
attr_reader :options, :socket
|
14
14
|
|
15
|
-
def initialize(options = {})
|
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,
|
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
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
:eof
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
69
|
+
raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
|
91
70
|
|
92
|
-
|
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
|
35
|
-
:authority
|
36
|
-
:path
|
37
|
-
:query
|
38
|
-
: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,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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
21
|
+
def stubs
|
22
|
+
@stubs ||= {}
|
23
|
+
end
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
7
|
-
:uri
|
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 "
|
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
|
77
|
+
:request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
|
77
78
|
)
|
78
79
|
end
|
79
80
|
|