faraday 2.0.0.alpha.pre.4 → 2.0.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.
@@ -1,241 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Faraday
4
- class Request
5
- # Catches exceptions and retries each request a limited number of times.
6
- #
7
- # By default, it retries 2 times and handles only timeout exceptions. It can
8
- # be configured with an arbitrary number of retries, a list of exceptions to
9
- # handle, a retry interval, a percentage of randomness to add to the retry
10
- # interval, and a backoff factor.
11
- #
12
- # @example Configure Retry middleware using intervals
13
- # Faraday.new do |conn|
14
- # conn.request(:retry, max: 2,
15
- # interval: 0.05,
16
- # interval_randomness: 0.5,
17
- # backoff_factor: 2,
18
- # exceptions: [CustomException, 'Timeout::Error'])
19
- #
20
- # conn.adapter(:net_http) # NB: Last middleware must be the adapter
21
- # end
22
- #
23
- # This example will result in a first interval that is random between 0.05
24
- # and 0.075 and a second interval that is random between 0.1 and 0.125.
25
- class Retry < Faraday::Middleware
26
- DEFAULT_EXCEPTIONS = [
27
- Errno::ETIMEDOUT, 'Timeout::Error',
28
- Faraday::TimeoutError, Faraday::RetriableResponse
29
- ].freeze
30
- IDEMPOTENT_METHODS = %i[delete get head options put].freeze
31
-
32
- # Options contains the configurable parameters for the Retry middleware.
33
- class Options < Faraday::Options.new(:max, :interval, :max_interval,
34
- :interval_randomness,
35
- :backoff_factor, :exceptions,
36
- :methods, :retry_if, :retry_block,
37
- :retry_statuses)
38
-
39
- DEFAULT_CHECK = ->(_env, _exception) { false }
40
-
41
- def self.from(value)
42
- if value.is_a?(Integer)
43
- new(value)
44
- else
45
- super(value)
46
- end
47
- end
48
-
49
- def max
50
- (self[:max] ||= 2).to_i
51
- end
52
-
53
- def interval
54
- (self[:interval] ||= 0).to_f
55
- end
56
-
57
- def max_interval
58
- (self[:max_interval] ||= Float::MAX).to_f
59
- end
60
-
61
- def interval_randomness
62
- (self[:interval_randomness] ||= 0).to_f
63
- end
64
-
65
- def backoff_factor
66
- (self[:backoff_factor] ||= 1).to_f
67
- end
68
-
69
- def exceptions
70
- Array(self[:exceptions] ||= DEFAULT_EXCEPTIONS)
71
- end
72
-
73
- def methods
74
- Array(self[:methods] ||= IDEMPOTENT_METHODS)
75
- end
76
-
77
- def retry_if
78
- self[:retry_if] ||= DEFAULT_CHECK
79
- end
80
-
81
- def retry_block
82
- self[:retry_block] ||= proc {}
83
- end
84
-
85
- def retry_statuses
86
- Array(self[:retry_statuses] ||= [])
87
- end
88
- end
89
-
90
- # @param app [#call]
91
- # @param options [Hash]
92
- # @option options [Integer] :max (2) Maximum number of retries
93
- # @option options [Integer] :interval (0) Pause in seconds between retries
94
- # @option options [Integer] :interval_randomness (0) The maximum random
95
- # interval amount expressed as a float between
96
- # 0 and 1 to use in addition to the interval.
97
- # @option options [Integer] :max_interval (Float::MAX) An upper limit
98
- # for the interval
99
- # @option options [Integer] :backoff_factor (1) The amount to multiply
100
- # each successive retry's interval amount by in order to provide backoff
101
- # @option options [Array] :exceptions ([ Errno::ETIMEDOUT,
102
- # 'Timeout::Error', Faraday::TimeoutError, Faraday::RetriableResponse])
103
- # The list of exceptions to handle. Exceptions can be given as
104
- # Class, Module, or String.
105
- # @option options [Array] :methods (the idempotent HTTP methods
106
- # in IDEMPOTENT_METHODS) A list of HTTP methods to retry without
107
- # calling retry_if. Pass an empty Array to call retry_if
108
- # for all exceptions.
109
- # @option options [Block] :retry_if (false) block that will receive
110
- # the env object and the exception raised
111
- # and should decide if the code should retry still the action or
112
- # not independent of the retry count. This would be useful
113
- # if the exception produced is non-recoverable or if the
114
- # the HTTP method called is not idempotent.
115
- # @option options [Block] :retry_block block that is executed before
116
- # every retry. Request environment, middleware options, current number
117
- # of retries and the exception is passed to the block as parameters.
118
- # @option options [Array] :retry_statuses Array of Integer HTTP status
119
- # codes or a single Integer value that determines whether to raise
120
- # a Faraday::RetriableResponse exception based on the HTTP status code
121
- # of an HTTP response.
122
- def initialize(app, options = nil)
123
- super(app)
124
- @options = Options.from(options)
125
- @errmatch = build_exception_matcher(@options.exceptions)
126
- end
127
-
128
- def calculate_sleep_amount(retries, env)
129
- retry_after = calculate_retry_after(env)
130
- retry_interval = calculate_retry_interval(retries)
131
-
132
- return if retry_after && retry_after > @options.max_interval
133
-
134
- if retry_after && retry_after >= retry_interval
135
- retry_after
136
- else
137
- retry_interval
138
- end
139
- end
140
-
141
- # @param env [Faraday::Env]
142
- def call(env)
143
- retries = @options.max
144
- request_body = env[:body]
145
- begin
146
- # after failure env[:body] is set to the response body
147
- env[:body] = request_body
148
- @app.call(env).tap do |resp|
149
- if @options.retry_statuses.include?(resp.status)
150
- raise Faraday::RetriableResponse.new(nil, resp)
151
- end
152
- end
153
- rescue @errmatch => e
154
- if retries.positive? && retry_request?(env, e)
155
- retries -= 1
156
- rewind_files(request_body)
157
- @options.retry_block.call(env, @options, retries, e)
158
- if (sleep_amount = calculate_sleep_amount(retries + 1, env))
159
- sleep sleep_amount
160
- retry
161
- end
162
- end
163
-
164
- raise unless e.is_a?(Faraday::RetriableResponse)
165
-
166
- e.response
167
- end
168
- end
169
-
170
- # An exception matcher for the rescue clause can usually be any object
171
- # that responds to `===`, but for Ruby 1.8 it has to be a Class or Module.
172
- #
173
- # @param exceptions [Array]
174
- # @api private
175
- # @return [Module] an exception matcher
176
- def build_exception_matcher(exceptions)
177
- matcher = Module.new
178
- (
179
- class << matcher
180
- self
181
- end).class_eval do
182
- define_method(:===) do |error|
183
- exceptions.any? do |ex|
184
- if ex.is_a? Module
185
- error.is_a? ex
186
- else
187
- Object.const_defined?(ex.to_s) && error.is_a?(Object.const_get(ex.to_s))
188
- end
189
- end
190
- end
191
- end
192
- matcher
193
- end
194
-
195
- private
196
-
197
- def retry_request?(env, exception)
198
- @options.methods.include?(env[:method]) ||
199
- @options.retry_if.call(env, exception)
200
- end
201
-
202
- def rewind_files(body)
203
- return unless body.is_a?(Hash)
204
-
205
- body.each do |_, value|
206
- value.rewind if value.is_a?(UploadIO)
207
- end
208
- end
209
-
210
- # MDN spec for Retry-After header:
211
- # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
212
- def calculate_retry_after(env)
213
- response_headers = env[:response_headers]
214
- return unless response_headers
215
-
216
- retry_after_value = env[:response_headers]['Retry-After']
217
-
218
- # Try to parse date from the header value
219
- begin
220
- datetime = DateTime.rfc2822(retry_after_value)
221
- datetime.to_time - Time.now.utc
222
- rescue ArgumentError
223
- retry_after_value.to_f
224
- end
225
- end
226
-
227
- def calculate_retry_interval(retries)
228
- retry_index = @options.max - retries
229
- current_interval = @options.interval *
230
- (@options.backoff_factor**retry_index)
231
- current_interval = [current_interval, @options.max_interval].min
232
- random_interval = rand * @options.interval_randomness.to_f *
233
- @options.interval
234
-
235
- current_interval + random_interval
236
- end
237
- end
238
- end
239
- end
240
-
241
- Faraday::Request.register_middleware(retry: Faraday::Request::Retry)
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'stringio'
4
-
5
- RSpec.describe Faraday::CompositeReadIO do
6
- Part = Struct.new(:to_io) do
7
- def length
8
- to_io.string.length
9
- end
10
- end
11
-
12
- def part(str)
13
- Part.new StringIO.new(str)
14
- end
15
-
16
- def composite_io(*parts)
17
- Faraday::CompositeReadIO.new(*parts)
18
- end
19
-
20
- context 'with empty composite_io' do
21
- subject { composite_io }
22
-
23
- it { expect(subject.length).to eq(0) }
24
- it { expect(subject.read).to eq('') }
25
- it { expect(subject.read(1)).to be_nil }
26
- end
27
-
28
- context 'with empty parts' do
29
- subject { composite_io(part(''), part('')) }
30
-
31
- it { expect(subject.length).to eq(0) }
32
- it { expect(subject.read).to eq('') }
33
- it { expect(subject.read(1)).to be_nil }
34
- end
35
-
36
- context 'with 2 parts' do
37
- subject { composite_io(part('abcd'), part('1234')) }
38
-
39
- it { expect(subject.length).to eq(8) }
40
- it { expect(subject.read).to eq('abcd1234') }
41
- it 'allows to read in chunks' do
42
- expect(subject.read(3)).to eq('abc')
43
- expect(subject.read(3)).to eq('d12')
44
- expect(subject.read(3)).to eq('34')
45
- expect(subject.read(3)).to be_nil
46
- end
47
- it 'allows to rewind while reading in chunks' do
48
- expect(subject.read(3)).to eq('abc')
49
- expect(subject.read(3)).to eq('d12')
50
- subject.rewind
51
- expect(subject.read(3)).to eq('abc')
52
- expect(subject.read(5)).to eq('d1234')
53
- expect(subject.read(3)).to be_nil
54
- subject.rewind
55
- expect(subject.read(2)).to eq('ab')
56
- end
57
- end
58
-
59
- context 'with mix of empty and non-empty parts' do
60
- subject { composite_io(part(''), part('abcd'), part(''), part('1234'), part('')) }
61
-
62
- it 'allows to read in chunks' do
63
- expect(subject.read(6)).to eq('abcd12')
64
- expect(subject.read(6)).to eq('34')
65
- expect(subject.read(6)).to be_nil
66
- end
67
- end
68
-
69
- context 'with utf8 multibyte part' do
70
- subject { composite_io(part("\x86"), part('ファイル')) }
71
-
72
- it { expect(subject.read).to eq(String.new("\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB", encoding: 'BINARY')) }
73
- it 'allows to read in chunks' do
74
- expect(subject.read(3)).to eq(String.new("\x86\xE3\x83", encoding: 'BINARY'))
75
- expect(subject.read(3)).to eq(String.new("\x95\xE3\x82", encoding: 'BINARY'))
76
- expect(subject.read(8)).to eq(String.new("\xA1\xE3\x82\xA4\xE3\x83\xAB", encoding: 'BINARY'))
77
- expect(subject.read(3)).to be_nil
78
- end
79
- end
80
- end
@@ -1,302 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- RSpec.describe Faraday::Request::Multipart do
4
- let(:options) { {} }
5
- let(:conn) do
6
- Faraday.new do |b|
7
- b.request :multipart, options
8
- b.request :url_encoded
9
- b.adapter :test do |stub|
10
- stub.post('/echo') do |env|
11
- posted_as = env[:request_headers]['Content-Type']
12
- expect(env[:body]).to be_a_kind_of(Faraday::CompositeReadIO)
13
- [200, { 'Content-Type' => posted_as }, env[:body].read]
14
- end
15
- end
16
- end
17
- end
18
-
19
- shared_examples 'a multipart request' do
20
- it 'generates a unique boundary for each request' do
21
- response1 = conn.post('/echo', payload)
22
- response2 = conn.post('/echo', payload)
23
-
24
- b1 = parse_multipart_boundary(response1.headers['Content-Type'])
25
- b2 = parse_multipart_boundary(response2.headers['Content-Type'])
26
- expect(b1).to_not eq(b2)
27
- end
28
- end
29
-
30
- context 'FilePart: when multipart objects in param' do
31
- let(:payload) do
32
- {
33
- a: 1,
34
- b: {
35
- c: Faraday::FilePart.new(__FILE__, 'text/x-ruby', nil,
36
- 'Content-Disposition' => 'form-data; foo=1'),
37
- d: 2
38
- }
39
- }
40
- end
41
- it_behaves_like 'a multipart request'
42
-
43
- it 'forms a multipart request' do
44
- response = conn.post('/echo', payload)
45
-
46
- boundary = parse_multipart_boundary(response.headers['Content-Type'])
47
- result = parse_multipart(boundary, response.body)
48
- expect(result[:errors]).to be_empty
49
-
50
- part_a, body_a = result.part('a')
51
- expect(part_a).to_not be_nil
52
- expect(part_a.filename).to be_nil
53
- expect(body_a).to eq('1')
54
-
55
- part_bc, body_bc = result.part('b[c]')
56
- expect(part_bc).to_not be_nil
57
- expect(part_bc.filename).to eq('multipart_spec.rb')
58
- expect(part_bc.headers['content-disposition'])
59
- .to eq(
60
- 'form-data; foo=1; name="b[c]"; filename="multipart_spec.rb"'
61
- )
62
- expect(part_bc.headers['content-type']).to eq('text/x-ruby')
63
- expect(part_bc.headers['content-transfer-encoding']).to eq('binary')
64
- expect(body_bc).to eq(File.read(__FILE__))
65
-
66
- part_bd, body_bd = result.part('b[d]')
67
- expect(part_bd).to_not be_nil
68
- expect(part_bd.filename).to be_nil
69
- expect(body_bd).to eq('2')
70
- end
71
- end
72
-
73
- context 'FilePart: when providing json and IO content in the same payload' do
74
- let(:io) { StringIO.new('io-content') }
75
- let(:json) do
76
- {
77
- b: 1,
78
- c: 2
79
- }.to_json
80
- end
81
-
82
- let(:payload) do
83
- {
84
- json: Faraday::ParamPart.new(json, 'application/json'),
85
- io: Faraday::FilePart.new(io, 'application/pdf')
86
- }
87
- end
88
-
89
- it_behaves_like 'a multipart request'
90
-
91
- it 'forms a multipart request' do
92
- response = conn.post('/echo', payload)
93
-
94
- boundary = parse_multipart_boundary(response.headers['Content-Type'])
95
- result = parse_multipart(boundary, response.body)
96
- expect(result[:errors]).to be_empty
97
-
98
- part_json, body_json = result.part('json')
99
- expect(part_json).to_not be_nil
100
- expect(part_json.mime).to eq('application/json')
101
- expect(part_json.filename).to be_nil
102
- expect(body_json).to eq(json)
103
-
104
- part_io, body_io = result.part('io')
105
- expect(part_io).to_not be_nil
106
- expect(part_io.mime).to eq('application/pdf')
107
- expect(part_io.filename).to eq('local.path')
108
- expect(body_io).to eq(io.string)
109
- end
110
- end
111
-
112
- context 'FilePart: when multipart objects in array param' do
113
- let(:payload) do
114
- {
115
- a: 1,
116
- b: [{
117
- c: Faraday::FilePart.new(__FILE__, 'text/x-ruby'),
118
- d: 2
119
- }]
120
- }
121
- end
122
-
123
- it_behaves_like 'a multipart request'
124
-
125
- it 'forms a multipart request' do
126
- response = conn.post('/echo', payload)
127
-
128
- boundary = parse_multipart_boundary(response.headers['Content-Type'])
129
- result = parse_multipart(boundary, response.body)
130
- expect(result[:errors]).to be_empty
131
-
132
- part_a, body_a = result.part('a')
133
- expect(part_a).to_not be_nil
134
- expect(part_a.filename).to be_nil
135
- expect(body_a).to eq('1')
136
-
137
- part_bc, body_bc = result.part('b[][c]')
138
- expect(part_bc).to_not be_nil
139
- expect(part_bc.filename).to eq('multipart_spec.rb')
140
- expect(part_bc.headers['content-disposition'])
141
- .to eq(
142
- 'form-data; name="b[][c]"; filename="multipart_spec.rb"'
143
- )
144
- expect(part_bc.headers['content-type']).to eq('text/x-ruby')
145
- expect(part_bc.headers['content-transfer-encoding']).to eq('binary')
146
- expect(body_bc).to eq(File.read(__FILE__))
147
-
148
- part_bd, body_bd = result.part('b[][d]')
149
- expect(part_bd).to_not be_nil
150
- expect(part_bd.filename).to be_nil
151
- expect(body_bd).to eq('2')
152
- end
153
- end
154
-
155
- context 'UploadIO: when multipart objects in param' do
156
- let(:payload) do
157
- {
158
- a: 1,
159
- b: {
160
- c: Faraday::FilePart.new(__FILE__, 'text/x-ruby', nil,
161
- 'Content-Disposition' => 'form-data; foo=1'),
162
- d: 2
163
- }
164
- }
165
- end
166
- it_behaves_like 'a multipart request'
167
-
168
- it 'forms a multipart request' do
169
- response = conn.post('/echo', payload)
170
-
171
- boundary = parse_multipart_boundary(response.headers['Content-Type'])
172
- result = parse_multipart(boundary, response.body)
173
- expect(result[:errors]).to be_empty
174
-
175
- part_a, body_a = result.part('a')
176
- expect(part_a).to_not be_nil
177
- expect(part_a.filename).to be_nil
178
- expect(body_a).to eq('1')
179
-
180
- part_bc, body_bc = result.part('b[c]')
181
- expect(part_bc).to_not be_nil
182
- expect(part_bc.filename).to eq('multipart_spec.rb')
183
- expect(part_bc.headers['content-disposition'])
184
- .to eq(
185
- 'form-data; foo=1; name="b[c]"; filename="multipart_spec.rb"'
186
- )
187
- expect(part_bc.headers['content-type']).to eq('text/x-ruby')
188
- expect(part_bc.headers['content-transfer-encoding']).to eq('binary')
189
- expect(body_bc).to eq(File.read(__FILE__))
190
-
191
- part_bd, body_bd = result.part('b[d]')
192
- expect(part_bd).to_not be_nil
193
- expect(part_bd.filename).to be_nil
194
- expect(body_bd).to eq('2')
195
- end
196
- end
197
-
198
- context 'UploadIO: when providing json and IO content in the same payload' do
199
- let(:io) { StringIO.new('io-content') }
200
- let(:json) do
201
- {
202
- b: 1,
203
- c: 2
204
- }.to_json
205
- end
206
-
207
- let(:payload) do
208
- {
209
- json: Faraday::ParamPart.new(json, 'application/json'),
210
- io: Faraday::FilePart.new(io, 'application/pdf')
211
- }
212
- end
213
-
214
- it_behaves_like 'a multipart request'
215
-
216
- it 'forms a multipart request' do
217
- response = conn.post('/echo', payload)
218
-
219
- boundary = parse_multipart_boundary(response.headers['Content-Type'])
220
- result = parse_multipart(boundary, response.body)
221
- expect(result[:errors]).to be_empty
222
-
223
- part_json, body_json = result.part('json')
224
- expect(part_json).to_not be_nil
225
- expect(part_json.mime).to eq('application/json')
226
- expect(part_json.filename).to be_nil
227
- expect(body_json).to eq(json)
228
-
229
- part_io, body_io = result.part('io')
230
- expect(part_io).to_not be_nil
231
- expect(part_io.mime).to eq('application/pdf')
232
- expect(part_io.filename).to eq('local.path')
233
- expect(body_io).to eq(io.string)
234
- end
235
- end
236
-
237
- context 'UploadIO: when multipart objects in array param' do
238
- let(:payload) do
239
- {
240
- a: 1,
241
- b: [{
242
- c: Faraday::FilePart.new(__FILE__, 'text/x-ruby'),
243
- d: 2
244
- }]
245
- }
246
- end
247
-
248
- it_behaves_like 'a multipart request'
249
-
250
- it 'forms a multipart request' do
251
- response = conn.post('/echo', payload)
252
-
253
- boundary = parse_multipart_boundary(response.headers['Content-Type'])
254
- result = parse_multipart(boundary, response.body)
255
- expect(result[:errors]).to be_empty
256
-
257
- part_a, body_a = result.part('a')
258
- expect(part_a).to_not be_nil
259
- expect(part_a.filename).to be_nil
260
- expect(body_a).to eq('1')
261
-
262
- part_bc, body_bc = result.part('b[][c]')
263
- expect(part_bc).to_not be_nil
264
- expect(part_bc.filename).to eq('multipart_spec.rb')
265
- expect(part_bc.headers['content-disposition'])
266
- .to eq(
267
- 'form-data; name="b[][c]"; filename="multipart_spec.rb"'
268
- )
269
- expect(part_bc.headers['content-type']).to eq('text/x-ruby')
270
- expect(part_bc.headers['content-transfer-encoding']).to eq('binary')
271
- expect(body_bc).to eq(File.read(__FILE__))
272
-
273
- part_bd, body_bd = result.part('b[][d]')
274
- expect(part_bd).to_not be_nil
275
- expect(part_bd.filename).to be_nil
276
- expect(body_bd).to eq('2')
277
- end
278
- end
279
-
280
- context 'when passing flat_encode=true option' do
281
- let(:options) { { flat_encode: true } }
282
- let(:io) { StringIO.new('io-content') }
283
- let(:payload) do
284
- {
285
- a: 1,
286
- b: [
287
- Faraday::FilePart.new(io, 'application/pdf'),
288
- Faraday::FilePart.new(io, 'application/pdf')
289
- ]
290
- }
291
- end
292
-
293
- it_behaves_like 'a multipart request'
294
-
295
- it 'encode params using flat encoder' do
296
- response = conn.post('/echo', payload)
297
-
298
- expect(response.body).to include('name="b"')
299
- expect(response.body).not_to include('name="b[]"')
300
- end
301
- end
302
- end