http 5.1.1 → 5.3.0
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 +6 -24
- data/.rubocop/metrics.yml +4 -0
- data/.rubocop/rspec.yml +9 -0
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +45 -32
- data/CHANGELOG.md +57 -0
- data/{CHANGES.md → CHANGES_OLD.md} +1 -1
- data/Gemfile +1 -0
- data/README.md +4 -2
- data/SECURITY.md +13 -1
- data/http.gemspec +8 -2
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +27 -3
- data/lib/http/client.rb +1 -1
- data/lib/http/connection.rb +12 -3
- data/lib/http/errors.rb +16 -0
- data/lib/http/feature.rb +2 -1
- data/lib/http/features/instrumentation.rb +6 -1
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +26 -40
- data/lib/http/request/writer.rb +2 -1
- data/lib/http/request.rb +15 -5
- data/lib/http/retriable/client.rb +37 -0
- data/lib/http/retriable/delay_calculator.rb +64 -0
- data/lib/http/retriable/errors.rb +14 -0
- data/lib/http/retriable/performer.rb +153 -0
- data/lib/http/timeout/null.rb +8 -5
- data/lib/http/uri.rb +18 -2
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +1 -0
- data/spec/lib/http/client_spec.rb +1 -0
- data/spec/lib/http/connection_spec.rb +23 -1
- data/spec/lib/http/features/instrumentation_spec.rb +19 -0
- data/spec/lib/http/features/raise_error_spec.rb +62 -0
- data/spec/lib/http/headers/normalizer_spec.rb +52 -0
- data/spec/lib/http/options/headers_spec.rb +5 -1
- data/spec/lib/http/redirector_spec.rb +6 -5
- data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
- data/spec/lib/http/retriable/performer_spec.rb +302 -0
- data/spec/lib/http/uri/normalizer_spec.rb +95 -0
- data/spec/lib/http_spec.rb +49 -3
- data/spec/spec_helper.rb +1 -0
- data/spec/support/dummy_server/servlet.rb +19 -6
- data/spec/support/dummy_server.rb +2 -1
- metadata +28 -8
data/lib/http/headers.rb
CHANGED
@@ -4,6 +4,7 @@ require "forwardable"
|
|
4
4
|
|
5
5
|
require "http/errors"
|
6
6
|
require "http/headers/mixin"
|
7
|
+
require "http/headers/normalizer"
|
7
8
|
require "http/headers/known"
|
8
9
|
|
9
10
|
module HTTP
|
@@ -12,12 +13,31 @@ module HTTP
|
|
12
13
|
extend Forwardable
|
13
14
|
include Enumerable
|
14
15
|
|
15
|
-
|
16
|
-
|
16
|
+
class << self
|
17
|
+
# Coerces given `object` into Headers.
|
18
|
+
#
|
19
|
+
# @raise [Error] if object can't be coerced
|
20
|
+
# @param [#to_hash, #to_h, #to_a] object
|
21
|
+
# @return [Headers]
|
22
|
+
def coerce(object)
|
23
|
+
unless object.is_a? self
|
24
|
+
object = case
|
25
|
+
when object.respond_to?(:to_hash) then object.to_hash
|
26
|
+
when object.respond_to?(:to_h) then object.to_h
|
27
|
+
when object.respond_to?(:to_a) then object.to_a
|
28
|
+
else raise Error, "Can't coerce #{object.inspect} to Headers"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
headers = new
|
32
|
+
object.each { |k, v| headers.add k, v }
|
33
|
+
headers
|
34
|
+
end
|
35
|
+
alias [] coerce
|
17
36
|
|
18
|
-
|
19
|
-
|
20
|
-
|
37
|
+
def normalizer
|
38
|
+
@normalizer ||= Headers::Normalizer.new
|
39
|
+
end
|
40
|
+
end
|
21
41
|
|
22
42
|
# Class constructor.
|
23
43
|
def initialize
|
@@ -194,45 +214,11 @@ module HTTP
|
|
194
214
|
dup.tap { |dupped| dupped.merge! other }
|
195
215
|
end
|
196
216
|
|
197
|
-
class << self
|
198
|
-
# Coerces given `object` into Headers.
|
199
|
-
#
|
200
|
-
# @raise [Error] if object can't be coerced
|
201
|
-
# @param [#to_hash, #to_h, #to_a] object
|
202
|
-
# @return [Headers]
|
203
|
-
def coerce(object)
|
204
|
-
unless object.is_a? self
|
205
|
-
object = case
|
206
|
-
when object.respond_to?(:to_hash) then object.to_hash
|
207
|
-
when object.respond_to?(:to_h) then object.to_h
|
208
|
-
when object.respond_to?(:to_a) then object.to_a
|
209
|
-
else raise Error, "Can't coerce #{object.inspect} to Headers"
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
headers = new
|
214
|
-
object.each { |k, v| headers.add k, v }
|
215
|
-
headers
|
216
|
-
end
|
217
|
-
alias [] coerce
|
218
|
-
end
|
219
|
-
|
220
217
|
private
|
221
218
|
|
222
219
|
# Transforms `name` to canonical HTTP header capitalization
|
223
|
-
#
|
224
|
-
# @param [String] name
|
225
|
-
# @raise [HeaderError] if normalized name does not
|
226
|
-
# match {HEADER_NAME_RE}
|
227
|
-
# @return [String] canonical HTTP header name
|
228
220
|
def normalize_header(name)
|
229
|
-
|
230
|
-
|
231
|
-
normalized = name.split(/[\-_]/).each(&:capitalize!).join("-")
|
232
|
-
|
233
|
-
return normalized if normalized =~ COMPLIANT_NAME_RE
|
234
|
-
|
235
|
-
raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
|
221
|
+
self.class.normalizer.call(name)
|
236
222
|
end
|
237
223
|
|
238
224
|
# Ensures there is no new line character in the header value
|
data/lib/http/request/writer.rb
CHANGED
@@ -108,6 +108,7 @@ module HTTP
|
|
108
108
|
|
109
109
|
private
|
110
110
|
|
111
|
+
# @raise [SocketWriteError] when unable to write to socket
|
111
112
|
def write(data)
|
112
113
|
until data.empty?
|
113
114
|
length = @socket.write(data)
|
@@ -118,7 +119,7 @@ module HTTP
|
|
118
119
|
rescue Errno::EPIPE
|
119
120
|
raise
|
120
121
|
rescue IOError, SocketError, SystemCallError => e
|
121
|
-
raise
|
122
|
+
raise SocketWriteError, "error writing to socket: #{e}", e.backtrace
|
122
123
|
end
|
123
124
|
end
|
124
125
|
end
|
data/lib/http/request.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "forwardable"
|
4
|
-
require "base64"
|
5
4
|
require "time"
|
6
5
|
|
6
|
+
require "http/base64"
|
7
7
|
require "http/errors"
|
8
8
|
require "http/headers"
|
9
9
|
require "http/request/body"
|
@@ -15,6 +15,7 @@ module HTTP
|
|
15
15
|
class Request
|
16
16
|
extend Forwardable
|
17
17
|
|
18
|
+
include HTTP::Base64
|
18
19
|
include HTTP::Headers::Mixin
|
19
20
|
|
20
21
|
# The method given was not understood
|
@@ -49,7 +50,10 @@ module HTTP
|
|
49
50
|
:search,
|
50
51
|
|
51
52
|
# RFC 4791: Calendaring Extensions to WebDAV -- CalDAV
|
52
|
-
:mkcalendar
|
53
|
+
:mkcalendar,
|
54
|
+
|
55
|
+
# Implemented by several caching servers, like Squid, Varnish or Fastly
|
56
|
+
:purge
|
53
57
|
].freeze
|
54
58
|
|
55
59
|
# Allowed schemes
|
@@ -156,7 +160,7 @@ module HTTP
|
|
156
160
|
end
|
157
161
|
|
158
162
|
def proxy_authorization_header
|
159
|
-
digest =
|
163
|
+
digest = encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}")
|
160
164
|
"Basic #{digest}"
|
161
165
|
end
|
162
166
|
|
@@ -172,7 +176,9 @@ module HTTP
|
|
172
176
|
uri.omit(:fragment)
|
173
177
|
else
|
174
178
|
uri.request_uri
|
175
|
-
end
|
179
|
+
end.to_s
|
180
|
+
|
181
|
+
raise RequestError, "Invalid request URI: #{request_uri.inspect}" if request_uri.match?(/\s/)
|
176
182
|
|
177
183
|
"#{verb.to_s.upcase} #{request_uri} HTTP/#{version}"
|
178
184
|
end
|
@@ -230,7 +236,11 @@ module HTTP
|
|
230
236
|
|
231
237
|
# @return [String] Default host (with port if needed) header value.
|
232
238
|
def default_host_header_value
|
233
|
-
PORTS[@scheme] == port ? host : "#{host}:#{port}"
|
239
|
+
value = PORTS[@scheme] == port ? host : "#{host}:#{port}"
|
240
|
+
|
241
|
+
raise RequestError, "Invalid host: #{value.inspect}" if value.match?(/\s/)
|
242
|
+
|
243
|
+
value
|
234
244
|
end
|
235
245
|
|
236
246
|
def prepare_body(body)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "http/retriable/performer"
|
4
|
+
|
5
|
+
module HTTP
|
6
|
+
module Retriable
|
7
|
+
# Retriable version of HTTP::Client.
|
8
|
+
#
|
9
|
+
# @see http://www.rubydoc.info/gems/http/HTTP/Client
|
10
|
+
class Client < HTTP::Client
|
11
|
+
# @param [Performer] performer
|
12
|
+
# @param [HTTP::Options, Hash] options
|
13
|
+
def initialize(performer, options)
|
14
|
+
@performer = performer
|
15
|
+
super(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Overriden version of `HTTP::Client#make_request`.
|
19
|
+
#
|
20
|
+
# Monitors request/response phase with performer.
|
21
|
+
#
|
22
|
+
# @see http://www.rubydoc.info/gems/http/HTTP/Client:perform
|
23
|
+
def perform(req, options)
|
24
|
+
@performer.perform(self, req) { super(req, options) }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Overriden version of `HTTP::Chainable#branch`.
|
30
|
+
#
|
31
|
+
# @return [HTTP::Retriable::Client]
|
32
|
+
def branch(options)
|
33
|
+
Retriable::Client.new(@performer, options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
module Retriable
|
5
|
+
# @api private
|
6
|
+
class DelayCalculator
|
7
|
+
def initialize(opts)
|
8
|
+
@max_delay = opts.fetch(:max_delay, Float::MAX).to_f
|
9
|
+
if (delay = opts[:delay]).respond_to?(:call)
|
10
|
+
@delay_proc = opts.fetch(:delay)
|
11
|
+
else
|
12
|
+
@delay = delay
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(iteration, response)
|
17
|
+
delay = if response && (retry_header = response.headers["Retry-After"])
|
18
|
+
delay_from_retry_header(retry_header)
|
19
|
+
else
|
20
|
+
calculate_delay_from_iteration(iteration)
|
21
|
+
end
|
22
|
+
|
23
|
+
ensure_dealy_in_bounds(delay)
|
24
|
+
end
|
25
|
+
|
26
|
+
RFC2822_DATE_REGEX = /^
|
27
|
+
(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s+
|
28
|
+
(?:0[1-9]|[1-2]?[0-9]|3[01])\s+
|
29
|
+
(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+
|
30
|
+
(?:19[0-9]{2}|[2-9][0-9]{3})\s+
|
31
|
+
(?:2[0-3]|[0-1][0-9]):(?:[0-5][0-9]):(?:60|[0-5][0-9])\s+
|
32
|
+
GMT
|
33
|
+
$/x
|
34
|
+
|
35
|
+
# Spec for Retry-After header
|
36
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
37
|
+
def delay_from_retry_header(value)
|
38
|
+
value = value.to_s.strip
|
39
|
+
|
40
|
+
case value
|
41
|
+
when RFC2822_DATE_REGEX then DateTime.rfc2822(value).to_time - Time.now.utc
|
42
|
+
when /^\d+$/ then value.to_i
|
43
|
+
else 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def calculate_delay_from_iteration(iteration)
|
48
|
+
if @delay_proc
|
49
|
+
@delay_proc.call(iteration)
|
50
|
+
elsif @delay
|
51
|
+
@delay
|
52
|
+
else
|
53
|
+
delay = (2**(iteration - 1)) - 1
|
54
|
+
delay_noise = rand
|
55
|
+
delay + delay_noise
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def ensure_dealy_in_bounds(delay)
|
60
|
+
delay.clamp(0, @max_delay)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require "http/retriable/errors"
|
5
|
+
require "http/retriable/delay_calculator"
|
6
|
+
require "openssl"
|
7
|
+
|
8
|
+
module HTTP
|
9
|
+
module Retriable
|
10
|
+
# Request performing watchdog.
|
11
|
+
# @api private
|
12
|
+
class Performer
|
13
|
+
# Exceptions we should retry
|
14
|
+
RETRIABLE_ERRORS = [
|
15
|
+
HTTP::TimeoutError,
|
16
|
+
HTTP::ConnectionError,
|
17
|
+
IO::EAGAINWaitReadable,
|
18
|
+
Errno::ECONNRESET,
|
19
|
+
Errno::ECONNREFUSED,
|
20
|
+
Errno::EHOSTUNREACH,
|
21
|
+
OpenSSL::SSL::SSLError,
|
22
|
+
EOFError,
|
23
|
+
IOError
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
# @param [Hash] opts
|
27
|
+
# @option opts [#to_i] :tries (5)
|
28
|
+
# @option opts [#call, #to_i] :delay (DELAY_PROC)
|
29
|
+
# @option opts [Array(Exception)] :exceptions (RETRIABLE_ERRORS)
|
30
|
+
# @option opts [Array(#to_i)] :retry_statuses
|
31
|
+
# @option opts [#call] :on_retry
|
32
|
+
# @option opts [#to_f] :max_delay (Float::MAX)
|
33
|
+
# @option opts [#call] :should_retry
|
34
|
+
def initialize(opts)
|
35
|
+
@exception_classes = opts.fetch(:exceptions, RETRIABLE_ERRORS)
|
36
|
+
@retry_statuses = opts[:retry_statuses]
|
37
|
+
@tries = opts.fetch(:tries, 5).to_i
|
38
|
+
@on_retry = opts.fetch(:on_retry, ->(*) {})
|
39
|
+
@should_retry_proc = opts[:should_retry]
|
40
|
+
@delay_calculator = DelayCalculator.new(opts)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Watches request/response execution.
|
44
|
+
#
|
45
|
+
# If any of {RETRIABLE_ERRORS} occur or response status is `5xx`, retries
|
46
|
+
# up to `:tries` amount of times. Sleeps for amount of seconds calculated
|
47
|
+
# with `:delay` proc before each retry.
|
48
|
+
#
|
49
|
+
# @see #initialize
|
50
|
+
# @api private
|
51
|
+
def perform(client, req, &block)
|
52
|
+
1.upto(Float::INFINITY) do |attempt| # infinite loop with index
|
53
|
+
err, res = try_request(&block)
|
54
|
+
|
55
|
+
if retry_request?(req, err, res, attempt)
|
56
|
+
begin
|
57
|
+
wait_for_retry_or_raise(req, err, res, attempt)
|
58
|
+
ensure
|
59
|
+
# Some servers support Keep-Alive on any response. Thus we should
|
60
|
+
# flush response before retry, to avoid state error (when socket
|
61
|
+
# has pending response data and we try to write new request).
|
62
|
+
# Alternatively, as we don't need response body here at all, we
|
63
|
+
# are going to close client, effectivle closing underlying socket
|
64
|
+
# and resetting client's state.
|
65
|
+
client.close
|
66
|
+
end
|
67
|
+
elsif err
|
68
|
+
client.close
|
69
|
+
raise err
|
70
|
+
elsif res
|
71
|
+
return res
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def calculate_delay(iteration, response)
|
77
|
+
@delay_calculator.call(iteration, response)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# rubocop:disable Lint/RescueException
|
83
|
+
def try_request
|
84
|
+
err, res = nil
|
85
|
+
|
86
|
+
begin
|
87
|
+
res = yield
|
88
|
+
rescue Exception => e
|
89
|
+
err = e
|
90
|
+
end
|
91
|
+
|
92
|
+
[err, res]
|
93
|
+
end
|
94
|
+
# rubocop:enable Lint/RescueException
|
95
|
+
|
96
|
+
def retry_request?(req, err, res, attempt)
|
97
|
+
if @should_retry_proc
|
98
|
+
@should_retry_proc.call(req, err, res, attempt)
|
99
|
+
elsif err
|
100
|
+
retry_exception?(err)
|
101
|
+
else
|
102
|
+
retry_response?(res)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def retry_exception?(err)
|
107
|
+
@exception_classes.any? { |e| err.is_a?(e) }
|
108
|
+
end
|
109
|
+
|
110
|
+
def retry_response?(res)
|
111
|
+
return false unless @retry_statuses
|
112
|
+
|
113
|
+
response_status = res.status.to_i
|
114
|
+
retry_matchers = [@retry_statuses].flatten
|
115
|
+
|
116
|
+
retry_matchers.any? do |matcher|
|
117
|
+
case matcher
|
118
|
+
when Range then matcher.cover?(response_status)
|
119
|
+
when Numeric then matcher == response_status
|
120
|
+
else matcher.call(response_status)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def wait_for_retry_or_raise(req, err, res, attempt)
|
126
|
+
if attempt < @tries
|
127
|
+
@on_retry.call(req, err, res)
|
128
|
+
sleep calculate_delay(attempt, res)
|
129
|
+
else
|
130
|
+
res&.flush
|
131
|
+
raise out_of_retries_error(req, res, err)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Builds OutOfRetriesError
|
136
|
+
#
|
137
|
+
# @param request [HTTP::Request]
|
138
|
+
# @param status [HTTP::Response, nil]
|
139
|
+
# @param exception [Exception, nil]
|
140
|
+
def out_of_retries_error(request, response, exception)
|
141
|
+
message = "#{request.verb.to_s.upcase} <#{request.uri}> failed"
|
142
|
+
|
143
|
+
message += " with #{response.status}" if response
|
144
|
+
message += ":#{exception}" if exception
|
145
|
+
|
146
|
+
HTTP::OutOfRetriesError.new(message).tap do |ex|
|
147
|
+
ex.cause = exception
|
148
|
+
ex.response = response
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
data/lib/http/timeout/null.rb
CHANGED
@@ -1,15 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "forwardable"
|
4
3
|
require "io/wait"
|
5
4
|
|
6
5
|
module HTTP
|
7
6
|
module Timeout
|
8
7
|
class Null
|
9
|
-
extend Forwardable
|
10
|
-
|
11
|
-
def_delegators :@socket, :close, :closed?
|
12
|
-
|
13
8
|
attr_reader :options, :socket
|
14
9
|
|
15
10
|
def initialize(options = {})
|
@@ -27,6 +22,14 @@ module HTTP
|
|
27
22
|
@socket.connect
|
28
23
|
end
|
29
24
|
|
25
|
+
def close
|
26
|
+
@socket&.close
|
27
|
+
end
|
28
|
+
|
29
|
+
def closed?
|
30
|
+
@socket&.closed?
|
31
|
+
end
|
32
|
+
|
30
33
|
# Configures the SSL connection and starts the connection
|
31
34
|
def start_tls(host, ssl_socket_class, ssl_context)
|
32
35
|
@socket = ssl_socket_class.new(socket, ssl_context)
|
data/lib/http/uri.rb
CHANGED
@@ -37,6 +37,9 @@ module HTTP
|
|
37
37
|
# @private
|
38
38
|
HTTPS_SCHEME = "https"
|
39
39
|
|
40
|
+
# @private
|
41
|
+
PERCENT_ENCODE = /[^\x21-\x7E]+/.freeze
|
42
|
+
|
40
43
|
# @private
|
41
44
|
NORMALIZER = lambda do |uri|
|
42
45
|
uri = HTTP::URI.parse uri
|
@@ -44,8 +47,8 @@ module HTTP
|
|
44
47
|
HTTP::URI.new(
|
45
48
|
:scheme => uri.normalized_scheme,
|
46
49
|
:authority => uri.normalized_authority,
|
47
|
-
:path => uri.
|
48
|
-
:query => uri.query,
|
50
|
+
:path => uri.path.empty? ? "/" : percent_encode(Addressable::URI.normalize_path(uri.path)),
|
51
|
+
:query => percent_encode(uri.query),
|
49
52
|
:fragment => uri.normalized_fragment
|
50
53
|
)
|
51
54
|
end
|
@@ -71,6 +74,19 @@ module HTTP
|
|
71
74
|
Addressable::URI.form_encode(form_values, sort)
|
72
75
|
end
|
73
76
|
|
77
|
+
# Percent-encode all characters matching a regular expression.
|
78
|
+
#
|
79
|
+
# @param [String] string raw string
|
80
|
+
#
|
81
|
+
# @return [String] encoded value
|
82
|
+
#
|
83
|
+
# @private
|
84
|
+
def self.percent_encode(string)
|
85
|
+
string&.gsub(PERCENT_ENCODE) do |substr|
|
86
|
+
substr.encode(Encoding::UTF_8).bytes.map { |c| format("%%%02X", c) }.join
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
74
90
|
# Creates an HTTP::URI instance from the given options
|
75
91
|
#
|
76
92
|
# @param [Hash, Addressable::URI] options_or_uri
|
data/lib/http/version.rb
CHANGED
data/lib/http.rb
CHANGED
@@ -261,6 +261,7 @@ RSpec.describe HTTP::Client do
|
|
261
261
|
|
262
262
|
expect(HTTP::Request).to receive(:new) do |opts|
|
263
263
|
expect(opts[:body]).to eq '{"foo":"bar"}'
|
264
|
+
expect(opts[:headers]["Content-Type"]).to eq "application/json; charset=utf-8"
|
264
265
|
end
|
265
266
|
|
266
267
|
client.get("http://example.com/", :json => {:foo => :bar})
|
@@ -8,11 +8,31 @@ RSpec.describe HTTP::Connection do
|
|
8
8
|
:headers => {}
|
9
9
|
)
|
10
10
|
end
|
11
|
-
let(:socket) { double(:connect => nil) }
|
11
|
+
let(:socket) { double(:connect => nil, :close => nil) }
|
12
12
|
let(:timeout_class) { double(:new => socket) }
|
13
13
|
let(:opts) { HTTP::Options.new(:timeout_class => timeout_class) }
|
14
14
|
let(:connection) { HTTP::Connection.new(req, opts) }
|
15
15
|
|
16
|
+
describe "#initialize times out" do
|
17
|
+
let(:req) do
|
18
|
+
HTTP::Request.new(
|
19
|
+
:verb => :get,
|
20
|
+
:uri => "https://example.com/",
|
21
|
+
:headers => {}
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
before do
|
26
|
+
expect(socket).to receive(:start_tls).and_raise(HTTP::TimeoutError)
|
27
|
+
expect(socket).to receive(:closed?) { false }
|
28
|
+
expect(socket).to receive(:close)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "closes the connection" do
|
32
|
+
expect { connection }.to raise_error(HTTP::TimeoutError)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
16
36
|
describe "#read_headers!" do
|
17
37
|
before do
|
18
38
|
connection.instance_variable_set(:@pending_response, true)
|
@@ -58,9 +78,11 @@ RSpec.describe HTTP::Connection do
|
|
58
78
|
connection.read_headers!
|
59
79
|
buffer = String.new
|
60
80
|
while (s = connection.readpartial(3))
|
81
|
+
expect(connection.finished_request?).to be false if s != ""
|
61
82
|
buffer << s
|
62
83
|
end
|
63
84
|
expect(buffer).to eq "1234567890"
|
85
|
+
expect(connection.finished_request?).to be true
|
64
86
|
end
|
65
87
|
end
|
66
88
|
end
|
@@ -59,4 +59,23 @@ RSpec.describe HTTP::Features::Instrumentation do
|
|
59
59
|
expect(instrumenter.output[:finish]).to eq(:response => response)
|
60
60
|
end
|
61
61
|
end
|
62
|
+
|
63
|
+
describe "logging errors" do
|
64
|
+
let(:request) do
|
65
|
+
HTTP::Request.new(
|
66
|
+
:verb => :post,
|
67
|
+
:uri => "https://example.com/",
|
68
|
+
:headers => {:accept => "application/json"},
|
69
|
+
:body => '{"hello": "world!"}'
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
let(:error) { HTTP::TimeoutError.new }
|
74
|
+
|
75
|
+
it "should log the error" do
|
76
|
+
feature.on_error(request, error)
|
77
|
+
|
78
|
+
expect(instrumenter.output[:finish]).to eq(:request => request, :error => error)
|
79
|
+
end
|
80
|
+
end
|
62
81
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Features::RaiseError do
|
4
|
+
subject(:feature) { described_class.new(ignore: ignore) }
|
5
|
+
|
6
|
+
let(:connection) { double }
|
7
|
+
let(:status) { 200 }
|
8
|
+
let(:ignore) { [] }
|
9
|
+
|
10
|
+
describe "#wrap_response" do
|
11
|
+
subject(:result) { feature.wrap_response(response) }
|
12
|
+
|
13
|
+
let(:response) do
|
14
|
+
HTTP::Response.new(
|
15
|
+
version: "1.1",
|
16
|
+
status: status,
|
17
|
+
headers: {},
|
18
|
+
connection: connection,
|
19
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when status is 200" do
|
24
|
+
it "returns original request" do
|
25
|
+
expect(result).to be response
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "when status is 399" do
|
30
|
+
let(:status) { 399 }
|
31
|
+
|
32
|
+
it "returns original request" do
|
33
|
+
expect(result).to be response
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when status is 400" do
|
38
|
+
let(:status) { 400 }
|
39
|
+
|
40
|
+
it "raises" do
|
41
|
+
expect { result }.to raise_error(HTTP::StatusError, "Unexpected status code 400")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when status is 599" do
|
46
|
+
let(:status) { 599 }
|
47
|
+
|
48
|
+
it "raises" do
|
49
|
+
expect { result }.to raise_error(HTTP::StatusError, "Unexpected status code 599")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "when error status is ignored" do
|
54
|
+
let(:status) { 500 }
|
55
|
+
let(:ignore) { [500] }
|
56
|
+
|
57
|
+
it "returns original request" do
|
58
|
+
expect(result).to be response
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|