httpx 1.8.3 → 1.9.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_8_4.md +13 -0
  3. data/doc/release_notes/1_9_0.md +75 -0
  4. data/lib/httpx/adapters/webmock.rb +1 -1
  5. data/lib/httpx/altsvc.rb +29 -15
  6. data/lib/httpx/callbacks.rb +2 -2
  7. data/lib/httpx/chainable.rb +6 -3
  8. data/lib/httpx/connection/http1.rb +11 -19
  9. data/lib/httpx/connection/http2.rb +112 -27
  10. data/lib/httpx/connection.rb +184 -46
  11. data/lib/httpx/headers.rb +2 -2
  12. data/lib/httpx/io/tcp.rb +18 -14
  13. data/lib/httpx/options.rb +86 -35
  14. data/lib/httpx/parser/http1.rb +3 -6
  15. data/lib/httpx/plugins/auth/digest.rb +1 -2
  16. data/lib/httpx/plugins/auth.rb +11 -2
  17. data/lib/httpx/plugins/aws_sdk_authentication.rb +7 -3
  18. data/lib/httpx/plugins/aws_sigv4.rb +1 -0
  19. data/lib/httpx/plugins/basic_auth.rb +2 -0
  20. data/lib/httpx/plugins/callbacks.rb +7 -6
  21. data/lib/httpx/plugins/circuit_breaker.rb +1 -0
  22. data/lib/httpx/plugins/cookies/cookie.rb +1 -1
  23. data/lib/httpx/plugins/cookies.rb +2 -2
  24. data/lib/httpx/plugins/digest_auth.rb +2 -0
  25. data/lib/httpx/plugins/expect.rb +2 -0
  26. data/lib/httpx/plugins/fiber_concurrency.rb +6 -0
  27. data/lib/httpx/plugins/follow_redirects.rb +8 -6
  28. data/lib/httpx/plugins/grpc/call.rb +4 -4
  29. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -2
  30. data/lib/httpx/plugins/grpc.rb +21 -0
  31. data/lib/httpx/plugins/hedging.rb +284 -0
  32. data/lib/httpx/plugins/ntlm_auth.rb +1 -0
  33. data/lib/httpx/plugins/ntlm_v2_auth.rb +1 -0
  34. data/lib/httpx/plugins/oauth.rb +1 -1
  35. data/lib/httpx/plugins/proxy/http.rb +71 -16
  36. data/lib/httpx/plugins/proxy/ssh.rb +30 -11
  37. data/lib/httpx/plugins/proxy.rb +66 -45
  38. data/lib/httpx/plugins/push_promise.rb +4 -2
  39. data/lib/httpx/plugins/retries.rb +13 -3
  40. data/lib/httpx/plugins/server_sent_events.rb +2 -1
  41. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  42. data/lib/httpx/plugins/stream.rb +2 -2
  43. data/lib/httpx/plugins/tracing.rb +61 -14
  44. data/lib/httpx/plugins/upgrade/h2.rb +1 -1
  45. data/lib/httpx/plugins/upgrade.rb +2 -0
  46. data/lib/httpx/plugins/webdav.rb +8 -0
  47. data/lib/httpx/request/body.rb +2 -1
  48. data/lib/httpx/request.rb +10 -7
  49. data/lib/httpx/resolver/multi.rb +2 -2
  50. data/lib/httpx/resolver/resolver.rb +2 -2
  51. data/lib/httpx/resolver.rb +5 -1
  52. data/lib/httpx/response/body.rb +2 -2
  53. data/lib/httpx/response.rb +19 -11
  54. data/lib/httpx/selector.rb +3 -3
  55. data/lib/httpx/session.rb +38 -11
  56. data/lib/httpx/transcoder/chunker.rb +2 -2
  57. data/lib/httpx/transcoder/json.rb +7 -7
  58. data/lib/httpx/version.rb +1 -1
  59. data/sig/altsvc.rbs +3 -3
  60. data/sig/callbacks.rbs +1 -0
  61. data/sig/chainable.rbs +1 -0
  62. data/sig/connection/http1.rbs +1 -1
  63. data/sig/connection/http2.rbs +30 -16
  64. data/sig/connection.rbs +19 -1
  65. data/sig/headers.rbs +1 -1
  66. data/sig/httpx.rbs +2 -0
  67. data/sig/options.rbs +43 -12
  68. data/sig/parser/http1.rbs +1 -1
  69. data/sig/plugins/auth/digest.rbs +0 -1
  70. data/sig/plugins/auth.rbs +12 -5
  71. data/sig/plugins/hedging.rbs +44 -0
  72. data/sig/plugins/proxy/http.rbs +8 -1
  73. data/sig/plugins/proxy.rbs +3 -2
  74. data/sig/plugins/retries.rbs +2 -2
  75. data/sig/plugins/tracing.rbs +14 -5
  76. data/sig/request/body.rbs +1 -1
  77. data/sig/request.rbs +2 -1
  78. data/sig/resolver/entry.rbs +2 -2
  79. data/sig/response.rbs +1 -2
  80. data/sig/selector.rbs +2 -2
  81. data/sig/session.rbs +7 -5
  82. metadata +7 -1
@@ -11,7 +11,7 @@ module HTTPX
11
11
  #
12
12
  # * HTTP proxies
13
13
  # * HTTPS proxies
14
- # * Socks4/4a proxies
14
+ # * Socks4 /Socks4a proxies
15
15
  # * Socks5 proxies
16
16
  #
17
17
  # https://gitlab.com/os85/httpx/wikis/Proxy
@@ -19,6 +19,7 @@ module HTTPX
19
19
  module Proxy
20
20
  class ProxyConnectionError < ProxyError; end
21
21
 
22
+ # :nodoc:
22
23
  PROXY_ERRORS = [TimeoutError, IOError, SystemCallError, Error].freeze
23
24
 
24
25
  class << self
@@ -39,21 +40,24 @@ module HTTPX
39
40
  end
40
41
  end
41
42
 
43
+ # Manages the proxy parameters and the proxy authentication flow.
42
44
  class Parameters
43
45
  attr_reader :uri, :username, :password, :scheme, :no_proxy
44
46
 
45
- def initialize(uri: nil, scheme: nil, username: nil, password: nil, no_proxy: nil, **extra)
47
+ def initialize(uri: nil, ns: 0, scheme: nil, username: nil, password: nil, no_proxy: nil, **extra)
46
48
  @no_proxy = Array(no_proxy) if no_proxy
47
49
  @uris = Array(uri)
48
- uri = @uris.first
50
+ uri = @uris[ns]
49
51
 
50
52
  @username = username
51
53
  @password = password
52
54
 
53
- @ns = 0
55
+ @initial_credentials = @username && @password
56
+
57
+ @ns = ns
54
58
 
55
59
  if uri
56
- @uri = uri.is_a?(URI::Generic) ? uri : URI(uri)
60
+ @uri = URI(uri)
57
61
  @username ||= @uri.user
58
62
  @password ||= @uri.password
59
63
  end
@@ -74,34 +78,27 @@ module HTTPX
74
78
  end
75
79
 
76
80
  def shift
77
- # TODO: this operation must be synchronized
78
- @ns += 1
79
- @uri = @uris[@ns]
80
-
81
- return unless @uri
82
-
83
- @uri = URI(@uri) unless @uri.is_a?(URI::Generic)
84
-
85
- scheme = infer_default_auth_scheme(@uri)
86
-
87
- return unless scheme != @scheme
88
-
89
- @scheme = scheme
90
- @username = username || @uri.user
91
- @password = password || @uri.password
92
- @authenticator = load_authenticator(scheme, @username, @password)
81
+ return if @ns >= @uris.size - 1
82
+
83
+ Parameters.new(
84
+ uri: @uris,
85
+ ns: @ns + 1,
86
+ username: (@username if @initial_credentials),
87
+ password: (@password if @initial_credentials),
88
+ no_proxy: @no_proxy
89
+ )
93
90
  end
94
91
 
95
- def can_authenticate?(*args)
92
+ def can_authenticate?(...)
96
93
  return false unless @authenticator && @authenticator.respond_to?(:can_authenticate?)
97
94
 
98
- @authenticator.can_authenticate?(*args)
95
+ @authenticator.can_authenticate?(...)
99
96
  end
100
97
 
101
- def authenticate(*args)
98
+ def authenticate(...)
102
99
  return unless @authenticator
103
100
 
104
- @authenticator.authenticate(*args)
101
+ @authenticator.authenticate(...)
105
102
  end
106
103
 
107
104
  def ==(other)
@@ -144,8 +141,26 @@ module HTTPX
144
141
 
145
142
  # adds support for the following options:
146
143
  #
147
- # :proxy :: proxy options defining *:uri*, *:username*, *:password* or
148
- # *:scheme* (i.e. <tt>{ uri: "http://proxy" }</tt>)
144
+ # :proxy :: hash of proxy options, where a URI an be passed containing most of the requred information,
145
+ # or instead the different proxy options can be passed independently.
146
+ #
147
+ # The following proxy options are supported:
148
+ #
149
+ # :uri :: proxy origin URI, or list of URIs; in the case of the latter, any failed attempt to connect to a list item
150
+ # will retry for the next item of the list, until all elements are exhausted or it times out meanwhile.
151
+ # <tt>:username</tt> and <tt>:password</tt> will be inferred from it when available. The URI scheme will identify
152
+ # the proxy type. (ex: <tt>"http://proxi-uri:8980"</tt>, <tt>"http://user:pass@proxy-uri:8181"</tt>,
153
+ # <tt>"socks5://user:pass@socks-proxy-uri:10300"</tt>, <tt>["http://proxi-uri:8980", "http://user:pass@proxy-uri:8181"]</tt>).
154
+ # :scheme :: identifies the authentication scheme to use with the proxy, if necessary; defaults to <tt>"basic"</tt>, for basic auth,
155
+ # can also be <tt>"digest"</tt> or <tt>"ntml"</tt>). Your recommended to use session methods like HTTP::InstanceMethods#with_proxy_basic_auth
156
+ # to set these instead.
157
+ # :username :: the username to use when authenticating with the proxy
158
+ # :password :: the password to use when authenticating with the proxy
159
+ # :no_proxy :: A list of hostnames (or regex patterns) for which the proxy should be bypassed
160
+ # (ex: <tt>["example.com", "localhost", /.*\.google\.com/]</tt>).
161
+ #
162
+ # if you systems setting environment variables like `HTTP_PROXY` or `NO_PROXY` though, you won't need to set
163
+ # most of the above.
149
164
  module OptionsMethods
150
165
  private
151
166
 
@@ -161,25 +176,31 @@ module HTTPX
161
176
  end
162
177
 
163
178
  module InstanceMethods
164
- def find_connection(request_uri, selector, options)
165
- return super unless options.respond_to?(:proxy)
179
+ def initialize(*)
180
+ super
181
+ @proxy_parameters = @options.proxy
182
+ end
183
+
184
+ def initialize_dup(other)
185
+ @proxy_parameters = other.instance_variable_get(:@proxy_parameters)
186
+ super
187
+ end
166
188
 
189
+ def find_connection(request_uri, selector, options)
167
190
  if (next_proxy = request_uri.find_proxy)
168
191
  return super(request_uri, selector, options.merge(proxy: Parameters.new(uri: next_proxy)))
169
192
  end
170
193
 
171
- proxy = options.proxy
172
-
173
- return super unless proxy
194
+ return super unless @proxy_parameters
174
195
 
175
- next_proxy = proxy.uri
196
+ next_proxy = @proxy_parameters.uri
176
197
 
177
198
  raise ProxyError, "Failed to connect to proxy" unless next_proxy
178
199
 
179
200
  raise ProxyError,
180
201
  "#{next_proxy.scheme}: unsupported proxy protocol" unless options.supported_proxy_protocols.include?(next_proxy.scheme)
181
202
 
182
- if (no_proxy = proxy.no_proxy)
203
+ if (no_proxy = @proxy_parameters.no_proxy)
183
204
  no_proxy = no_proxy.join(",") if no_proxy.is_a?(Array)
184
205
 
185
206
  unless no_proxy != "*" && # NO_PROXY=* bypasses proxy use
@@ -189,7 +210,7 @@ module HTTPX
189
210
  end
190
211
  end
191
212
 
192
- super(request_uri, selector, options.merge(proxy: proxy))
213
+ super(request_uri, selector, options.merge(proxy: @proxy_parameters))
193
214
  end
194
215
 
195
216
  private
@@ -201,10 +222,10 @@ module HTTPX
201
222
  response = super
202
223
 
203
224
  if response.is_a?(ErrorResponse) && proxy_error?(request, response, options)
204
- options.proxy.shift
225
+ @proxy_parameters = options.proxy.shift
205
226
 
206
227
  # return last error response if no more proxies to try
207
- return response if options.proxy.uri.nil?
228
+ return response unless @proxy_parameters
208
229
 
209
230
  log { "failed connecting to proxy, trying next..." }
210
231
  request.transition(:idle)
@@ -222,22 +243,22 @@ module HTTPX
222
243
  end
223
244
  end
224
245
 
225
- def proxy_error?(_request, response, options)
226
- return false unless options.proxy
246
+ def proxy_error?(_request, response, _options)
247
+ proxy_parameters = @proxy_parameters
248
+
249
+ return false unless proxy_parameters
227
250
 
228
251
  error = response.error
229
252
  case error
230
253
  when NativeResolveError
231
- proxy_uri = URI(options.proxy.uri)
254
+ proxy_uri = proxy_parameters.uri
232
255
 
233
256
  unresolved_host = error.host
234
257
 
235
258
  # failed resolving proxy domain
236
259
  unresolved_host == proxy_uri.host
237
260
  when ResolveError
238
- proxy_uri = URI(options.proxy.uri)
239
-
240
- error.message.end_with?(proxy_uri.to_s)
261
+ error.message.end_with?(proxy_parameters.uri.to_s)
241
262
  when ProxyConnectionError
242
263
  # timeout errors connecting to proxy
243
264
  true
@@ -256,7 +277,7 @@ module HTTPX
256
277
 
257
278
  # redefining the connection origin as the proxy's URI,
258
279
  # as this will be used as the tcp peer ip.
259
- @proxy_uri = URI(@options.proxy.uri)
280
+ @proxy_uri = @options.proxy.uri
260
281
  end
261
282
 
262
283
  def peer
@@ -312,7 +333,7 @@ module HTTPX
312
333
  when :idle
313
334
  transition(:connecting)
314
335
  when :connected
315
- transition(:open)
336
+ super
316
337
  end
317
338
  end
318
339
 
@@ -12,8 +12,10 @@ module HTTPX
12
12
  #
13
13
  module PushPromise
14
14
  def self.extra_options(options)
15
- options.merge(http2_settings: { settings_enable_push: 1 },
16
- max_concurrent_requests: 1)
15
+ options.merge(
16
+ http2_settings: { settings_enable_push: 1 },
17
+ max_concurrent_requests: 1
18
+ )
17
19
  end
18
20
 
19
21
  module ResponseMethods
@@ -31,11 +31,21 @@ module HTTPX
31
31
  TLSError,
32
32
  Zlib::BufError,
33
33
  PingTimeoutError,
34
+ # HTTP/2 GOAWAY or RST_STREAM errors guaranteed to be retriable for all
35
+ # types of requests.
36
+ Connection::HTTP2::RefusedStreamError,
34
37
  Connection::HTTP2::GoawayError,
35
38
  Connection::HTTP2::PingError,
39
+
40
+ # happens if connections were somehow sent to a closed or closing
41
+ # HTTP2::Connection, request guaranteed to not have been processed.
42
+ ::HTTP2::Error::ConnectionClosed,
36
43
  ].freeze
37
44
 
38
45
  RETRYABLE_ERRORS = (RECONNECTABLE_ERRORS + [
46
+ # most RST_STREAM errors aren't safely retriable, as request may have
47
+ # been partially processed.
48
+ Connection::HTTP2::RstStreamError,
39
49
  Parser::Error,
40
50
  TimeoutError,
41
51
  ]).freeze
@@ -155,7 +165,7 @@ module HTTPX
155
165
  # retry-after must be calculated before prepare_to_retry, as it relies on
156
166
  # state changed byit.
157
167
  retry_after = when_to_retry(request, response, options)
158
- prepare_to_retry(request, response)
168
+ prepare_to_retry(request, response, retry_after)
159
169
 
160
170
  if retry_after&.positive?
161
171
  retry_start = Utils.now
@@ -197,7 +207,7 @@ module HTTPX
197
207
  RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) } && !ex.is_a?(TotalRequestTimeoutError)
198
208
  end
199
209
 
200
- def prepare_to_retry(request, response)
210
+ def prepare_to_retry(request, response, _retry_after)
201
211
  request.retries -= 1 unless can_reconnect?(request, response)
202
212
  request.transition(:idle)
203
213
  end
@@ -256,7 +266,7 @@ module HTTPX
256
266
  attr_writer :partial_response
257
267
 
258
268
  # initializes the request instance, sets the number of retries for the request.
259
- def initialize(*args)
269
+ def initialize(*)
260
270
  super
261
271
  @retries = @options.max_retries
262
272
  @partial_response = nil
@@ -40,7 +40,8 @@ module HTTPX
40
40
  end
41
41
 
42
42
  module InstanceMethods
43
- def request(*args, **options)
43
+ # sends a stream request.
44
+ def request(*, **options)
44
45
  options[:stream] = true if options[:event_stream]
45
46
 
46
47
  super
@@ -142,7 +142,7 @@ module HTTPX
142
142
  end
143
143
 
144
144
  def addresses=(addrs)
145
- addrs.reject! do |ipaddr|
145
+ addrs = addrs.reject do |ipaddr|
146
146
  ipaddr = ipaddr.address
147
147
  next false if @options.safe_private_ranges&.any? { |r| r.include?(ipaddr) }
148
148
 
@@ -111,10 +111,10 @@ module HTTPX
111
111
  end || super
112
112
  end
113
113
 
114
- def method_missing(meth, *args, **kwargs, &block)
114
+ def method_missing(meth, ...)
115
115
  return super unless response.respond_to?(meth)
116
116
 
117
- response.__send__(meth, *args, **kwargs, &block)
117
+ response.__send__(meth, ...)
118
118
  end
119
119
  end
120
120
 
@@ -36,9 +36,9 @@ module HTTPX::Plugins
36
36
  %i[start finish reset enabled?].each do |callback|
37
37
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
38
38
  # proxies ##{callback} calls to wrapper tracers.
39
- def #{callback}(*args) # def start(*args)
40
- @tracers.each { |t| t.#{callback}(*args) } # @tracers.each { |t| t.start(*args) }
41
- end # end
39
+ def #{callback}(...) # def start(...)
40
+ @tracers.each { |t| t.#{callback}(...) } # @tracers.each { |t| t.start(....) }
41
+ end # end
42
42
  OUT
43
43
  end
44
44
  end
@@ -63,13 +63,18 @@ module HTTPX::Plugins
63
63
  end
64
64
 
65
65
  module RequestMethods
66
+ # time at which a request started being buffered for sending.
66
67
  attr_accessor :init_time
67
68
 
69
+ # when not nil, it means this request was sent to a non-open connection, and contains how much time it took
70
+ # the connection to connect.
71
+ attr_accessor :handshake_time
72
+
68
73
  # intercepts request initialization to inject the tracing logic.
69
74
  def initialize(*)
70
75
  super
71
76
 
72
- @init_time = nil
77
+ @init_time = @handshake_time = nil
73
78
 
74
79
  tracer = @options.tracer
75
80
 
@@ -79,12 +84,13 @@ module HTTPX::Plugins
79
84
  tracer.reset(self)
80
85
 
81
86
  # request is reset when it's retried.
82
- @init_time = nil
87
+ @init_time = @handshake_time = nil
83
88
  end
84
89
  on(:headers) do
85
90
  # the usual request init time (when not including the connection handshake)
86
91
  # should be the time the request is buffered the first time.
87
- @init_time ||= ::Time.now.utc
92
+ @init_time = ::Time.now.utc
93
+ @init_time -= @handshake_time if @handshake_time
88
94
 
89
95
  tracer.start(self)
90
96
  end
@@ -92,12 +98,15 @@ module HTTPX::Plugins
92
98
  end
93
99
 
94
100
  def response=(*)
95
- # init_time should be set when it's send to a connection.
96
- # However, there are situations where connection initialization fails.
101
+ # There are situations where connection initialization fails.
97
102
  # Example is the :ssrf_filter plugin, which raises an error on
98
103
  # initialize if the host is an IP which matches against the known set.
99
104
  # in such cases, we'll just set here right here.
100
- @init_time ||= ::Time.now.utc
105
+ unless @init_time
106
+ @init_time = ::Time.now.utc
107
+
108
+ @init_time -= @handshake_time if @handshake_time
109
+ end
101
110
 
102
111
  super
103
112
  end
@@ -112,11 +121,11 @@ module HTTPX::Plugins
112
121
  end
113
122
 
114
123
  def send_request_to_parser(request)
115
- if connecting?
116
- @init_time ||= ::Time.now.utc
117
-
118
- # request span timeframe should include the time it took to connect.
119
- request.init_time ||= @init_time
124
+ if connecting? && @init_time
125
+ # at this point, the connection has connected, but hasn't yet transitioned to :open,
126
+ # so it's the time the requests which caused this connection to connect, are finally being
127
+ # sent to the parser, so handshake time should factor into the span time.
128
+ request.handshake_time ||= ::Time.now.utc - @init_time
120
129
  end
121
130
 
122
131
  super
@@ -137,6 +146,17 @@ module HTTPX::Plugins
137
146
 
138
147
  private
139
148
 
149
+ def handle_error(er, request = nil)
150
+ if connecting? && @init_time
151
+ # bookkeep handshake time back into pending requests, which were never initialized with it.
152
+ handshake_time = ::Time.now.utc - @init_time
153
+ request.handshake_time ||= handshake_time if request
154
+ @pending.each { |req| req.handshake_time ||= handshake_time }
155
+ end
156
+
157
+ super
158
+ end
159
+
140
160
  def connect
141
161
  @init_time ||= ::Time.now.utc
142
162
 
@@ -151,6 +171,33 @@ module HTTPX::Plugins
151
171
  super
152
172
  end
153
173
  end
174
+
175
+ module HTTP2Methods
176
+ def initialize(*)
177
+ super
178
+
179
+ @handshake_time = nil
180
+ @handshake_init_time = ::Time.now.utc
181
+ end
182
+
183
+ def send(request, head = false)
184
+ if head
185
+ # only true for pending requests waiting on the handshake, so handshake time accrues.
186
+ request.handshake_time ||= 0.0
187
+ request.handshake_time += @handshake_time
188
+ end
189
+
190
+ super
191
+ end
192
+
193
+ private
194
+
195
+ def on_settings(*)
196
+ @handshake_time = ::Time.now.utc - @handshake_init_time unless @handshake_completed
197
+
198
+ super
199
+ end
200
+ end
154
201
  end
155
202
  register_plugin :tracing, Tracing
156
203
  end
@@ -25,7 +25,7 @@ module HTTPX
25
25
  def interests
26
26
  return super unless connecting? && @parser
27
27
 
28
- connect
28
+ try_connect
29
29
 
30
30
  return @io.interests if connecting?
31
31
 
@@ -30,6 +30,8 @@ module HTTPX
30
30
  end
31
31
 
32
32
  module InstanceMethods
33
+ private
34
+
33
35
  def fetch_response(request, selector, options)
34
36
  response = super
35
37
 
@@ -13,14 +13,18 @@ module HTTPX
13
13
  end
14
14
 
15
15
  module InstanceMethods
16
+ # sends a COPY WebDav request for +src+ and +dest+.
16
17
  def copy(src, dest)
17
18
  request("COPY", src, headers: { "destination" => @options.origin.merge(dest) })
18
19
  end
19
20
 
21
+ # sends a MOVE WebDav request for +src+ and +dest+.
20
22
  def move(src, dest)
21
23
  request("MOVE", src, headers: { "destination" => @options.origin.merge(dest) })
22
24
  end
23
25
 
26
+ # sends a LOCK WebDav request for +path+ (for the time set by +timeout+, when passed).
27
+ # it calls #unlock after the block is executed.
24
28
  def lock(path, timeout: nil, &blk)
25
29
  headers = {}
26
30
  headers["timeout"] = if timeout && timeout.positive?
@@ -51,14 +55,17 @@ module HTTPX
51
55
  response
52
56
  end
53
57
 
58
+ # sends a UNLOCK WebDav request for +path+, using the given +lock_token+.
54
59
  def unlock(path, lock_token)
55
60
  request("UNLOCK", path, headers: { "lock-token" => lock_token })
56
61
  end
57
62
 
63
+ # sends a MKCOL WebDav request for +dir+.
58
64
  def mkcol(dir)
59
65
  request("MKCOL", dir)
60
66
  end
61
67
 
68
+ # sends a PROPFIND WebDav request for +path+.
62
69
  def propfind(path, xml = nil)
63
70
  body = case xml
64
71
  when :acl
@@ -73,6 +80,7 @@ module HTTPX
73
80
  request("PROPFIND", path, headers: { "depth" => "1" }, xml: body)
74
81
  end
75
82
 
83
+ # sends a PROPPATCH WebDav request for +path+.
76
84
  def proppatch(path, xml)
77
85
  body = "<?xml version=\"1.0\"?>" \
78
86
  "<D:propertyupdate xmlns:D=\"DAV:\" xmlns:Z=\"http://ns.example.com/standards/z39.50/\">#{xml}</D:propertyupdate>"
@@ -32,6 +32,7 @@ module HTTPX
32
32
  @headers = h
33
33
  @body = self.class.initialize_body(params)
34
34
  @options = options.merge(params)
35
+ @unbounded_body = nil
35
36
 
36
37
  if @body
37
38
  if @options.compress_request_body && @headers.key?("content-encoding")
@@ -102,7 +103,7 @@ module HTTPX
102
103
 
103
104
  # returns whether the body yields infinitely.
104
105
  def unbounded_body?
105
- return @unbounded_body if defined?(@unbounded_body)
106
+ return @unbounded_body unless @unbounded_body.nil?
106
107
 
107
108
  @unbounded_body = !@body.nil? && (chunked? || @body.bytesize == Float::INFINITY)
108
109
  end
data/lib/httpx/request.rb CHANGED
@@ -107,7 +107,7 @@ module HTTPX
107
107
  @connection = @response =
108
108
  @drainer = @peer_address = @callbacks =
109
109
  @informational_status = @on_response_arrived = nil
110
- @ping = @started = false
110
+ @ping = @started = @complete = false
111
111
  @persistent = @options.persistent
112
112
  @active_timeouts = []
113
113
  end
@@ -121,8 +121,11 @@ module HTTPX
121
121
  end
122
122
 
123
123
  def complete!(response = @response)
124
+ return false if @complete
125
+
124
126
  emit(:complete, response)
125
127
  reset_timers(true)
128
+ @complete = true
126
129
  end
127
130
 
128
131
  # whether request has been buffered with a ping
@@ -137,22 +140,22 @@ module HTTPX
137
140
 
138
141
  # the read timeout defined for this request.
139
142
  def read_timeout
140
- @options.timeout[:read_timeout]
143
+ @options.timeout.read_timeout
141
144
  end
142
145
 
143
146
  # the write timeout defined for this request.
144
147
  def write_timeout
145
- @options.timeout[:write_timeout]
148
+ @options.timeout.write_timeout
146
149
  end
147
150
 
148
151
  # the request timeout defined for this request.
149
152
  def request_timeout
150
- @options.timeout[:request_timeout]
153
+ @options.timeout.request_timeout
151
154
  end
152
155
 
153
156
  # the total request timeout defined for this request.
154
157
  def total_request_timeout
155
- @options.timeout[:total_request_timeout]
158
+ @options.timeout.total_request_timeout
156
159
  end
157
160
 
158
161
  def persistent?
@@ -296,7 +299,7 @@ module HTTPX
296
299
  case nextstate
297
300
  when :idle
298
301
  @body.rewind
299
- @ping = false
302
+ @ping = @complete = false
300
303
  @response = @drainer = nil
301
304
 
302
305
  # request may be sent to a different connection and will be
@@ -336,7 +339,7 @@ module HTTPX
336
339
 
337
340
  # whether the request supports the 100-continue handshake and already processed the 100 response.
338
341
  def expects?
339
- @headers["expect"] == "100-continue" && @informational_status == 100 && !@response
342
+ @headers.key?("expect") && @headers.get("expect").include?("100-continue") && @informational_status == 100 && !@response
340
343
  end
341
344
 
342
345
  def set_timeout_callback(event, &callback)
@@ -35,8 +35,8 @@ module HTTPX
35
35
  @resolvers.each { |r| r.current_session = s }
36
36
  end
37
37
 
38
- def log(*args, **kwargs, &blk)
39
- @resolvers.each { |r| r.log(*args, **kwargs, &blk) }
38
+ def log(...)
39
+ @resolvers.each { |r| r.log(...) }
40
40
  end
41
41
 
42
42
  def closed?
@@ -56,9 +56,9 @@ module HTTPX
56
56
  call
57
57
  end
58
58
 
59
- def force_close(*args)
59
+ def force_close(...)
60
60
  while (connection = @connections.shift)
61
- connection.force_close(*args)
61
+ connection.force_close(...)
62
62
  end
63
63
  end
64
64
 
@@ -57,7 +57,11 @@ module HTTPX
57
57
 
58
58
  if message.rcode != Resolv::DNS::RCode::NoError
59
59
  case message.rcode
60
- when Resolv::DNS::RCode::ServFail
60
+ when Resolv::DNS::RCode::ServFail,
61
+ # getaddrinfo also retries these errors, as they're not about the hostname being resolved,
62
+ # but the DNS resolver state.
63
+ Resolv::DNS::RCode::NotImp,
64
+ Resolv::DNS::RCode::Refused
61
65
  return :retriable_error, message.rcode
62
66
  else
63
67
  return :dns_error, message.rcode
@@ -69,7 +69,7 @@ module HTTPX
69
69
  end
70
70
 
71
71
  # reads a chunk from the payload (implementation of the IO reader protocol).
72
- def read(*args)
72
+ def read(...)
73
73
  return unless @buffer
74
74
 
75
75
  unless @reader
@@ -77,7 +77,7 @@ module HTTPX
77
77
  @reader = @buffer
78
78
  end
79
79
 
80
- @reader.read(*args)
80
+ @reader.read(...)
81
81
  end
82
82
 
83
83
  # size of the decoded response payload. May differ from "content-length" header if