httpx 1.4.4 → 1.5.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_5_0.md +126 -0
  3. data/lib/httpx/adapters/datadog.rb +24 -3
  4. data/lib/httpx/adapters/webmock.rb +1 -0
  5. data/lib/httpx/buffer.rb +16 -5
  6. data/lib/httpx/connection/http1.rb +8 -9
  7. data/lib/httpx/connection/http2.rb +48 -24
  8. data/lib/httpx/connection.rb +36 -19
  9. data/lib/httpx/errors.rb +2 -11
  10. data/lib/httpx/headers.rb +24 -23
  11. data/lib/httpx/io/ssl.rb +2 -1
  12. data/lib/httpx/io/tcp.rb +9 -7
  13. data/lib/httpx/io/unix.rb +1 -1
  14. data/lib/httpx/loggable.rb +13 -1
  15. data/lib/httpx/options.rb +63 -48
  16. data/lib/httpx/parser/http1.rb +1 -1
  17. data/lib/httpx/plugins/aws_sigv4.rb +1 -0
  18. data/lib/httpx/plugins/callbacks.rb +19 -6
  19. data/lib/httpx/plugins/circuit_breaker.rb +4 -3
  20. data/lib/httpx/plugins/cookies/jar.rb +0 -2
  21. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +7 -4
  22. data/lib/httpx/plugins/cookies.rb +4 -4
  23. data/lib/httpx/plugins/follow_redirects.rb +4 -2
  24. data/lib/httpx/plugins/grpc/call.rb +1 -1
  25. data/lib/httpx/plugins/h2c.rb +7 -1
  26. data/lib/httpx/plugins/persistent.rb +22 -1
  27. data/lib/httpx/plugins/proxy/http.rb +3 -1
  28. data/lib/httpx/plugins/query.rb +35 -0
  29. data/lib/httpx/plugins/response_cache/file_store.rb +115 -15
  30. data/lib/httpx/plugins/response_cache/store.rb +7 -67
  31. data/lib/httpx/plugins/response_cache.rb +179 -29
  32. data/lib/httpx/plugins/retries.rb +26 -14
  33. data/lib/httpx/plugins/stream.rb +4 -2
  34. data/lib/httpx/plugins/stream_bidi.rb +315 -0
  35. data/lib/httpx/pool.rb +58 -5
  36. data/lib/httpx/request/body.rb +1 -1
  37. data/lib/httpx/request.rb +6 -2
  38. data/lib/httpx/resolver/https.rb +10 -4
  39. data/lib/httpx/resolver/native.rb +13 -13
  40. data/lib/httpx/resolver/resolver.rb +4 -0
  41. data/lib/httpx/resolver/system.rb +37 -14
  42. data/lib/httpx/resolver.rb +2 -2
  43. data/lib/httpx/response/body.rb +10 -21
  44. data/lib/httpx/response/buffer.rb +36 -12
  45. data/lib/httpx/response.rb +11 -1
  46. data/lib/httpx/selector.rb +16 -12
  47. data/lib/httpx/session.rb +79 -19
  48. data/lib/httpx/timers.rb +24 -16
  49. data/lib/httpx/transcoder/multipart/decoder.rb +4 -2
  50. data/lib/httpx/transcoder/multipart/encoder.rb +2 -1
  51. data/lib/httpx/version.rb +1 -1
  52. data/sig/buffer.rbs +1 -1
  53. data/sig/chainable.rbs +5 -2
  54. data/sig/connection/http2.rbs +11 -2
  55. data/sig/connection.rbs +4 -4
  56. data/sig/errors.rbs +0 -3
  57. data/sig/headers.rbs +15 -10
  58. data/sig/httpx.rbs +5 -1
  59. data/sig/io/tcp.rbs +6 -0
  60. data/sig/loggable.rbs +2 -0
  61. data/sig/options.rbs +7 -1
  62. data/sig/plugins/cookies/cookie.rbs +1 -3
  63. data/sig/plugins/cookies/jar.rbs +4 -4
  64. data/sig/plugins/cookies/set_cookie_parser.rbs +22 -0
  65. data/sig/plugins/cookies.rbs +2 -0
  66. data/sig/plugins/h2c.rbs +4 -0
  67. data/sig/plugins/proxy/http.rbs +3 -0
  68. data/sig/plugins/proxy.rbs +4 -0
  69. data/sig/plugins/query.rbs +18 -0
  70. data/sig/plugins/response_cache/file_store.rbs +19 -0
  71. data/sig/plugins/response_cache/store.rbs +13 -0
  72. data/sig/plugins/response_cache.rbs +41 -19
  73. data/sig/plugins/retries.rbs +4 -3
  74. data/sig/plugins/stream.rbs +5 -1
  75. data/sig/plugins/stream_bidi.rbs +68 -0
  76. data/sig/plugins/upgrade/h2.rbs +9 -0
  77. data/sig/plugins/upgrade.rbs +5 -0
  78. data/sig/pool.rbs +5 -0
  79. data/sig/punycode.rbs +5 -0
  80. data/sig/request.rbs +2 -0
  81. data/sig/resolver/https.rbs +3 -2
  82. data/sig/resolver/native.rbs +1 -2
  83. data/sig/resolver/resolver.rbs +11 -3
  84. data/sig/resolver/system.rbs +19 -2
  85. data/sig/resolver.rbs +11 -7
  86. data/sig/response/body.rbs +3 -4
  87. data/sig/response/buffer.rbs +2 -3
  88. data/sig/response.rbs +2 -2
  89. data/sig/selector.rbs +20 -10
  90. data/sig/session.rbs +14 -6
  91. data/sig/timers.rbs +5 -7
  92. data/sig/transcoder/multipart.rbs +4 -3
  93. metadata +13 -2
@@ -1,39 +1,139 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pathname"
4
- require_relative "store"
5
4
 
6
5
  module HTTPX::Plugins
7
6
  module ResponseCache
8
- class FileStore < Store
7
+ # Implementation of a file system based cache store.
8
+ #
9
+ # It stores cached responses in a file under a directory pointed by the +dir+
10
+ # variable (defaults to the default temp directory from the OS), in a custom
11
+ # format (similar but different from HTTP/1.1 request/response framing).
12
+ class FileStore
13
+ CRLF = HTTPX::Connection::HTTP1::CRLF
14
+
15
+ attr_reader :dir
16
+
9
17
  def initialize(dir = Dir.tmpdir)
10
- @dir = Pathname.new(dir)
18
+ @dir = Pathname.new(dir).join("httpx-response-cache")
19
+
20
+ FileUtils.mkdir_p(@dir)
11
21
  end
12
22
 
13
23
  def clear
14
- # delete all files
24
+ FileUtils.rm_rf(@dir)
15
25
  end
16
26
 
17
- def cached?(request)
18
- file_path = @dir.join(request.response_cache_key)
27
+ def get(request)
28
+ path = file_path(request)
29
+
30
+ return unless File.exist?(path)
31
+
32
+ File.open(path, mode: File::RDONLY | File::BINARY) do |f|
33
+ f.flock(File::Constants::LOCK_SH)
19
34
 
20
- exist?(file_path)
35
+ read_from_file(request, f)
36
+ end
21
37
  end
22
38
 
23
- private
39
+ def set(request, response)
40
+ path = file_path(request)
41
+
42
+ file_exists = File.exist?(path)
43
+
44
+ mode = file_exists ? File::RDWR : File::CREAT | File::Constants::WRONLY
45
+
46
+ File.open(path, mode: mode | File::BINARY) do |f|
47
+ f.flock(File::Constants::LOCK_EX)
48
+
49
+ if file_exists
50
+ cached_response = read_from_file(request, f)
51
+
52
+ if cached_response
53
+ next if cached_response == request.cached_response
54
+
55
+ cached_response.close
56
+
57
+ f.truncate(0)
58
+
59
+ f.rewind
60
+ end
61
+ end
62
+ # cache the request headers
63
+ f << request.verb << CRLF
64
+ f << request.uri << CRLF
65
+
66
+ request.headers.each do |field, value|
67
+ f << field << ":" << value << CRLF
68
+ end
69
+
70
+ f << CRLF
71
+
72
+ # cache the response
73
+ f << response.status << CRLF
74
+ f << response.version << CRLF
24
75
 
25
- def _get(request)
26
- return unless cached?(request)
76
+ response.headers.each do |field, value|
77
+ f << field << ":" << value << CRLF
78
+ end
27
79
 
28
- File.open(@dir.join(request.response_cache_key))
80
+ f << CRLF
81
+
82
+ response.body.rewind
83
+
84
+ ::IO.copy_stream(response.body, f)
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def file_path(request)
91
+ @dir.join(request.response_cache_key)
29
92
  end
30
93
 
31
- def _set(request, response)
32
- file_path = @dir.join(request.response_cache_key)
94
+ def read_from_file(request, f)
95
+ # if it's an empty file
96
+ return if f.eof?
97
+
98
+ # read request data
99
+ verb = f.readline.delete_suffix!(CRLF)
100
+ uri = f.readline.delete_suffix!(CRLF)
101
+
102
+ request_headers = {}
103
+ while (line = f.readline) != CRLF
104
+ line.delete_suffix!(CRLF)
105
+ sep_index = line.index(":")
106
+
107
+ field = line.byteslice(0..(sep_index - 1))
108
+ value = line.byteslice((sep_index + 1)..-1)
109
+
110
+ request_headers[field] = value
111
+ end
112
+
113
+ status = f.readline.delete_suffix!(CRLF)
114
+ version = f.readline.delete_suffix!(CRLF)
115
+
116
+ response_headers = {}
117
+ while (line = f.readline) != CRLF
118
+ line.delete_suffix!(CRLF)
119
+ sep_index = line.index(":")
120
+
121
+ field = line.byteslice(0..(sep_index - 1))
122
+ value = line.byteslice((sep_index + 1)..-1)
123
+
124
+ response_headers[field] = value
125
+ end
126
+
127
+ original_request = request.options.request_class.new(verb, uri, request.options)
128
+ original_request.merge_headers(request_headers)
129
+
130
+ response = request.options.response_class.new(request, status, version, response_headers)
131
+ response.original_request = original_request
132
+ response.finish!
33
133
 
34
- response.copy_to(file_path)
134
+ ::IO.copy_stream(f, response.body)
35
135
 
36
- response.body.rewind
136
+ response
37
137
  end
38
138
  end
39
139
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module HTTPX::Plugins
4
4
  module ResponseCache
5
+ # Implementation of a thread-safe in-memory cache store.
5
6
  class Store
6
7
  def initialize
7
8
  @store = {}
@@ -12,80 +13,19 @@ module HTTPX::Plugins
12
13
  @store_mutex.synchronize { @store.clear }
13
14
  end
14
15
 
15
- def lookup(request)
16
- responses = _get(request)
17
-
18
- return unless responses
19
-
20
- responses.find(&method(:match_by_vary?).curry(2)[request])
21
- end
22
-
23
- def cached?(request)
24
- lookup(request)
25
- end
26
-
27
- def cache(request, response)
28
- return unless ResponseCache.cacheable_request?(request) && ResponseCache.cacheable_response?(response)
29
-
30
- _set(request, response)
31
- end
32
-
33
- def prepare(request)
34
- cached_response = lookup(request)
35
-
36
- return unless cached_response
37
-
38
- return unless match_by_vary?(request, cached_response)
39
-
40
- if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
41
- request.headers.add("if-modified-since", last_modified)
42
- end
43
-
44
- if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"]) # rubocop:disable Style/GuardClause
45
- request.headers.add("if-none-match", etag)
46
- end
47
- end
48
-
49
- private
50
-
51
- def match_by_vary?(request, response)
52
- vary = response.vary
53
-
54
- return true unless vary
55
-
56
- original_request = response.instance_variable_get(:@request)
57
-
58
- return request.headers.same_headers?(original_request.headers) if vary == %w[*]
59
-
60
- vary.all? do |cache_field|
61
- cache_field.downcase!
62
- !original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
63
- end
64
- end
65
-
66
- def _get(request)
16
+ def get(request)
67
17
  @store_mutex.synchronize do
68
- responses = @store[request.response_cache_key]
69
-
70
- return unless responses
71
-
72
- responses.select! do |res|
73
- !res.body.closed? && res.fresh?
74
- end
75
-
76
- responses
18
+ @store[request.response_cache_key]
77
19
  end
78
20
  end
79
21
 
80
- def _set(request, response)
22
+ def set(request, response)
81
23
  @store_mutex.synchronize do
82
- responses = (@store[request.response_cache_key] ||= [])
24
+ cached_response = @store[request.response_cache_key]
83
25
 
84
- responses.reject! do |res|
85
- res.body.closed? || !res.fresh? || match_by_vary?(request, res)
86
- end
26
+ cached_response.close if cached_response
87
27
 
88
- responses << response
28
+ @store[request.response_cache_key] = response
89
29
  end
90
30
  end
91
31
  end
@@ -10,21 +10,18 @@ module HTTPX
10
10
  module ResponseCache
11
11
  CACHEABLE_VERBS = %w[GET HEAD].freeze
12
12
  CACHEABLE_STATUS_CODES = [200, 203, 206, 300, 301, 410].freeze
13
+ SUPPORTED_VARY_HEADERS = %w[accept accept-encoding accept-language cookie origin].sort.freeze
13
14
  private_constant :CACHEABLE_VERBS
14
15
  private_constant :CACHEABLE_STATUS_CODES
15
16
 
16
17
  class << self
17
18
  def load_dependencies(*)
18
19
  require_relative "response_cache/store"
20
+ require_relative "response_cache/file_store"
19
21
  end
20
22
 
21
- def cacheable_request?(request)
22
- CACHEABLE_VERBS.include?(request.verb) &&
23
- (
24
- !request.headers.key?("cache-control") || !request.headers.get("cache-control").include?("no-store")
25
- )
26
- end
27
-
23
+ # whether the +response+ can be stored in the response cache.
24
+ # (i.e. has a cacheable body, does not contain directives prohibiting storage, etc...)
28
25
  def cacheable_response?(response)
29
26
  response.is_a?(Response) &&
30
27
  (
@@ -39,82 +36,230 @@ module HTTPX
39
36
  # directive prohibits caching. However, a cache that does not support
40
37
  # the Range and Content-Range headers MUST NOT cache 206 (Partial
41
38
  # Content) responses.
42
- response.status != 206 && (
43
- response.headers.key?("etag") || response.headers.key?("last-modified") || response.fresh?
44
- )
39
+ response.status != 206
45
40
  end
46
41
 
47
- def cached_response?(response)
42
+ # whether the +response+
43
+ def not_modified?(response)
48
44
  response.is_a?(Response) && response.status == 304
49
45
  end
50
46
 
51
47
  def extra_options(options)
52
- options.merge(response_cache_store: Store.new)
48
+ options.merge(
49
+ supported_vary_headers: SUPPORTED_VARY_HEADERS,
50
+ response_cache_store: :store,
51
+ )
53
52
  end
54
53
  end
55
54
 
55
+ # adds support for the following options:
56
+ #
57
+ # :supported_vary_headers :: array of header values that will be considered for a "vary" header based cache validation
58
+ # (defaults to {SUPPORTED_VARY_HEADERS}).
59
+ # :response_cache_store :: object where cached responses are fetch from or stored in; defaults to <tt>:store</tt> (in-memory
60
+ # cache), can be set to <tt>:file_store</tt> (file system cache store) as well, or any object which
61
+ # abides by the Cache Store Interface
62
+ #
63
+ # The Cache Store Interface requires implementation of the following methods:
64
+ #
65
+ # * +#get(request) -> response or nil+
66
+ # * +#set(request, response) -> void+
67
+ # * +#clear() -> void+)
68
+ #
56
69
  module OptionsMethods
57
70
  def option_response_cache_store(value)
58
- raise TypeError, "must be an instance of #{Store}" unless value.is_a?(Store)
71
+ case value
72
+ when :store
73
+ Store.new
74
+ when :file_store
75
+ FileStore.new
76
+ else
77
+ value
78
+ end
79
+ end
59
80
 
60
- value
81
+ def option_supported_vary_headers(value)
82
+ Array(value).sort
61
83
  end
62
84
  end
63
85
 
64
86
  module InstanceMethods
87
+ # wipes out all cached responses from the cache store.
65
88
  def clear_response_cache
66
89
  @options.response_cache_store.clear
67
90
  end
68
91
 
69
92
  def build_request(*)
70
93
  request = super
71
- return request unless ResponseCache.cacheable_request?(request) && @options.response_cache_store.cached?(request)
94
+ return request unless cacheable_request?(request)
72
95
 
73
- @options.response_cache_store.prepare(request)
96
+ prepare_cache(request)
74
97
 
75
98
  request
76
99
  end
77
100
 
101
+ private
102
+
103
+ def send_request(request, *)
104
+ return request if request.response
105
+
106
+ super
107
+ end
108
+
78
109
  def fetch_response(request, *)
79
110
  response = super
80
111
 
81
112
  return unless response
82
113
 
83
- if ResponseCache.cached_response?(response)
114
+ if ResponseCache.not_modified?(response)
84
115
  log { "returning cached response for #{request.uri}" }
85
- cached_response = @options.response_cache_store.lookup(request)
86
-
87
- response.copy_from_cached(cached_response)
88
116
 
89
- else
90
- @options.response_cache_store.cache(request, response)
117
+ response.copy_from_cached!
118
+ elsif request.cacheable_verb? && ResponseCache.cacheable_response?(response)
119
+ request.options.response_cache_store.set(request, response) unless response.cached?
91
120
  end
92
121
 
93
122
  response
94
123
  end
124
+
125
+ # will either assign a still-fresh cached response to +request+, or set up its HTTP
126
+ # cache invalidation headers in case it's not fresh anymore.
127
+ def prepare_cache(request)
128
+ cached_response = request.options.response_cache_store.get(request)
129
+
130
+ return unless cached_response && match_by_vary?(request, cached_response)
131
+
132
+ cached_response.body.rewind
133
+
134
+ if cached_response.fresh?
135
+ cached_response = cached_response.dup
136
+ cached_response.mark_as_cached!
137
+ request.response = cached_response
138
+ request.emit(:response, cached_response)
139
+ return
140
+ end
141
+
142
+ request.cached_response = cached_response
143
+
144
+ if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
145
+ request.headers.add("if-modified-since", last_modified)
146
+ end
147
+
148
+ if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"]) # rubocop:disable Style/GuardClause
149
+ request.headers.add("if-none-match", etag)
150
+ end
151
+ end
152
+
153
+ def cacheable_request?(request)
154
+ request.cacheable_verb? &&
155
+ (
156
+ !request.headers.key?("cache-control") || !request.headers.get("cache-control").include?("no-store")
157
+ )
158
+ end
159
+
160
+ # whether the +response+ complies with the directives set by the +request+ "vary" header
161
+ # (true when none is available).
162
+ def match_by_vary?(request, response)
163
+ vary = response.vary
164
+
165
+ return true unless vary
166
+
167
+ original_request = response.original_request
168
+
169
+ if vary == %w[*]
170
+ request.options.supported_vary_headers.each do |field|
171
+ return false unless request.headers[field] == original_request.headers[field]
172
+ end
173
+
174
+ return true
175
+ end
176
+
177
+ vary.all? do |field|
178
+ !original_request.headers.key?(field) || request.headers[field] == original_request.headers[field]
179
+ end
180
+ end
95
181
  end
96
182
 
97
183
  module RequestMethods
184
+ # points to a previously cached Response corresponding to this request.
185
+ attr_accessor :cached_response
186
+
187
+ def initialize(*)
188
+ super
189
+ @cached_response = nil
190
+ end
191
+
192
+ def merge_headers(*)
193
+ super
194
+ @response_cache_key = nil
195
+ end
196
+
197
+ # returns whether this request is cacheable as per HTTP caching rules.
198
+ def cacheable_verb?
199
+ CACHEABLE_VERBS.include?(@verb)
200
+ end
201
+
202
+ # returns a unique cache key as a String identifying this request
98
203
  def response_cache_key
99
- @response_cache_key ||= Digest::SHA1.hexdigest("httpx-response-cache-#{@verb}-#{@uri}")
204
+ @response_cache_key ||= begin
205
+ keys = [@verb, @uri]
206
+
207
+ @options.supported_vary_headers.each do |field|
208
+ value = @headers[field]
209
+
210
+ keys << value if value
211
+ end
212
+ Digest::SHA1.hexdigest("httpx-response-cache-#{keys.join("-")}")
213
+ end
100
214
  end
101
215
  end
102
216
 
103
217
  module ResponseMethods
104
- def copy_from_cached(other)
218
+ attr_writer :original_request
219
+
220
+ def initialize(*)
221
+ super
222
+ @cached = false
223
+ end
224
+
225
+ # a copy of the request this response was originally cached from
226
+ def original_request
227
+ @original_request || @request
228
+ end
229
+
230
+ # whether this Response was duplicated from a previously {RequestMethods#cached_response}.
231
+ def cached?
232
+ @cached
233
+ end
234
+
235
+ # sets this Response as being duplicated from a previously cached response.
236
+ def mark_as_cached!
237
+ @cached = true
238
+ end
239
+
240
+ # eager-copies the response headers and body from {RequestMethods#cached_response}.
241
+ def copy_from_cached!
242
+ cached_response = @request.cached_response
243
+
244
+ return unless cached_response
245
+
105
246
  # 304 responses do not have content-type, which are needed for decoding.
106
- @headers = @headers.class.new(other.headers.merge(@headers))
247
+ @headers = @headers.class.new(cached_response.headers.merge(@headers))
107
248
 
108
- @body = other.body.dup
249
+ @body = cached_response.body.dup
109
250
 
110
251
  @body.rewind
111
252
  end
112
253
 
113
254
  # A response is fresh if its age has not yet exceeded its freshness lifetime.
255
+ # other (#cache_control} directives may influence the outcome, as per the rules
256
+ # from the {rfc}[https://www.rfc-editor.org/rfc/rfc7234]
114
257
  def fresh?
115
258
  if cache_control
116
259
  return false if cache_control.include?("no-cache")
117
260
 
261
+ return true if cache_control.include?("immutable")
262
+
118
263
  # check age: max-age
119
264
  max_age = cache_control.find { |directive| directive.start_with?("s-maxage") }
120
265
 
@@ -132,15 +277,16 @@ module HTTPX
132
277
  begin
133
278
  expires = Time.httpdate(@headers["expires"])
134
279
  rescue ArgumentError
135
- return true
280
+ return false
136
281
  end
137
282
 
138
283
  return (expires - Time.now).to_i.positive?
139
284
  end
140
285
 
141
- true
286
+ false
142
287
  end
143
288
 
289
+ # returns the "cache-control" directives as an Array of String(s).
144
290
  def cache_control
145
291
  return @cache_control if defined?(@cache_control)
146
292
 
@@ -151,24 +297,28 @@ module HTTPX
151
297
  end
152
298
  end
153
299
 
300
+ # returns the "vary" header value as an Array of (String) headers.
154
301
  def vary
155
302
  return @vary if defined?(@vary)
156
303
 
157
304
  @vary = begin
158
305
  return unless @headers.key?("vary")
159
306
 
160
- @headers["vary"].split(/ *, */)
307
+ @headers["vary"].split(/ *, */).map(&:downcase)
161
308
  end
162
309
  end
163
310
 
164
311
  private
165
312
 
313
+ # returns the value of the "age" header as an Integer (time since epoch).
314
+ # if no "age" of header exists, it returns the number of seconds since {#date}.
166
315
  def age
167
316
  return @headers["age"].to_i if @headers.key?("age")
168
317
 
169
318
  (Time.now - date).to_i
170
319
  end
171
320
 
321
+ # returns the value of the "date" header as a Time object
172
322
  def date
173
323
  @date ||= Time.httpdate(@headers["date"])
174
324
  rescue NoMethodError, ArgumentError
@@ -17,7 +17,9 @@ module HTTPX
17
17
  # TODO: pass max_retries in a configure/load block
18
18
 
19
19
  IDEMPOTENT_METHODS = %w[GET OPTIONS HEAD PUT DELETE].freeze
20
- RETRYABLE_ERRORS = [
20
+
21
+ # subset of retryable errors which are safe to retry when reconnecting
22
+ RECONNECTABLE_ERRORS = [
21
23
  IOError,
22
24
  EOFError,
23
25
  Errno::ECONNRESET,
@@ -25,12 +27,15 @@ module HTTPX
25
27
  Errno::EPIPE,
26
28
  Errno::EINVAL,
27
29
  Errno::ETIMEDOUT,
28
- Parser::Error,
29
- TLSError,
30
- TimeoutError,
31
30
  ConnectionError,
32
- Connection::HTTP2::GoawayError,
31
+ TLSError,
32
+ Connection::HTTP2::Error,
33
33
  ].freeze
34
+
35
+ RETRYABLE_ERRORS = (RECONNECTABLE_ERRORS + [
36
+ Parser::Error,
37
+ TimeoutError,
38
+ ]).freeze
34
39
  DEFAULT_JITTER = ->(interval) { interval * ((rand + 1) * 0.5) }
35
40
 
36
41
  if ENV.key?("HTTPX_NO_JITTER")
@@ -88,6 +93,7 @@ module HTTPX
88
93
  end
89
94
 
90
95
  module InstanceMethods
96
+ # returns a `:retries` plugin enabled session with +n+ maximum retries per request setting.
91
97
  def max_retries(n)
92
98
  with(max_retries: n)
93
99
  end
@@ -99,16 +105,16 @@ module HTTPX
99
105
 
100
106
  if response &&
101
107
  request.retries.positive? &&
102
- __repeatable_request?(request, options) &&
108
+ repeatable_request?(request, options) &&
103
109
  (
104
110
  (
105
- response.is_a?(ErrorResponse) && __retryable_error?(response.error)
111
+ response.is_a?(ErrorResponse) && retryable_error?(response.error)
106
112
  ) ||
107
113
  (
108
114
  options.retry_on && options.retry_on.call(response)
109
115
  )
110
116
  )
111
- __try_partial_retry(request, response)
117
+ try_partial_retry(request, response)
112
118
  log { "failed to get response, #{request.retries} tries to go..." }
113
119
  request.retries -= 1 unless request.ping? # do not exhaust retries on connection liveness probes
114
120
  request.transition(:idle)
@@ -125,9 +131,10 @@ module HTTPX
125
131
  retry_start = Utils.now
126
132
  log { "retrying after #{retry_after} secs..." }
127
133
  selector.after(retry_after) do
128
- if request.response
134
+ if (response = request.response)
135
+ response.finish!
129
136
  # request has terminated abruptly meanwhile
130
- request.emit(:response, request.response)
137
+ request.emit(:response, response)
131
138
  else
132
139
  log { "retrying (elapsed time: #{Utils.elapsed_time(retry_start)})!!" }
133
140
  send_request(request, selector, options)
@@ -142,11 +149,13 @@ module HTTPX
142
149
  response
143
150
  end
144
151
 
145
- def __repeatable_request?(request, options)
152
+ # returns whether +request+ can be retried.
153
+ def repeatable_request?(request, options)
146
154
  IDEMPOTENT_METHODS.include?(request.verb) || options.retry_change_requests
147
155
  end
148
156
 
149
- def __retryable_error?(ex)
157
+ # returns whether the +ex+ exception happend for a retriable request.
158
+ def retryable_error?(ex)
150
159
  RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
151
160
  end
152
161
 
@@ -155,11 +164,11 @@ module HTTPX
155
164
  end
156
165
 
157
166
  #
158
- # Atttempt to set the request to perform a partial range request.
167
+ # Attempt to set the request to perform a partial range request.
159
168
  # This happens if the peer server accepts byte-range requests, and
160
169
  # the last response contains some body payload.
161
170
  #
162
- def __try_partial_retry(request, response)
171
+ def try_partial_retry(request, response)
163
172
  response = response.response if response.is_a?(ErrorResponse)
164
173
 
165
174
  return unless response
@@ -180,10 +189,13 @@ module HTTPX
180
189
  end
181
190
 
182
191
  module RequestMethods
192
+ # number of retries left.
183
193
  attr_accessor :retries
184
194
 
195
+ # a response partially received before.
185
196
  attr_writer :partial_response
186
197
 
198
+ # initializes the request instance, sets the number of retries for the request.
187
199
  def initialize(*args)
188
200
  super
189
201
  @retries = @options.max_retries