faraday 1.9.3 → 2.0.0.alpha.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -1
  3. data/README.md +16 -9
  4. data/examples/client_test.rb +1 -1
  5. data/lib/faraday/adapter/test.rb +2 -0
  6. data/lib/faraday/adapter.rb +0 -5
  7. data/lib/faraday/connection.rb +3 -84
  8. data/lib/faraday/encoders/nested_params_encoder.rb +2 -2
  9. data/lib/faraday/error.rb +7 -0
  10. data/lib/faraday/file_part.rb +122 -0
  11. data/lib/faraday/logging/formatter.rb +1 -0
  12. data/lib/faraday/middleware.rb +0 -1
  13. data/lib/faraday/middleware_registry.rb +15 -79
  14. data/lib/faraday/options.rb +3 -3
  15. data/lib/faraday/param_part.rb +53 -0
  16. data/lib/faraday/rack_builder.rb +1 -1
  17. data/lib/faraday/request/authorization.rb +26 -40
  18. data/lib/faraday/request/instrumentation.rb +2 -0
  19. data/lib/faraday/request/json.rb +55 -0
  20. data/lib/faraday/request/multipart.rb +108 -0
  21. data/lib/faraday/request/retry.rb +241 -0
  22. data/lib/faraday/request/url_encoded.rb +2 -0
  23. data/lib/faraday/request.rb +13 -29
  24. data/lib/faraday/response/json.rb +54 -0
  25. data/lib/faraday/response/logger.rb +4 -4
  26. data/lib/faraday/response/raise_error.rb +9 -1
  27. data/lib/faraday/response.rb +8 -19
  28. data/lib/faraday/utils/headers.rb +1 -1
  29. data/lib/faraday/utils.rb +9 -4
  30. data/lib/faraday/version.rb +1 -1
  31. data/lib/faraday.rb +8 -45
  32. data/spec/faraday/connection_spec.rb +78 -51
  33. data/spec/faraday/options/env_spec.rb +2 -2
  34. data/spec/faraday/rack_builder_spec.rb +5 -43
  35. data/spec/faraday/request/authorization_spec.rb +14 -36
  36. data/spec/faraday/request/instrumentation_spec.rb +5 -7
  37. data/spec/faraday/request/json_spec.rb +111 -0
  38. data/spec/faraday/request/multipart_spec.rb +302 -0
  39. data/spec/faraday/request/retry_spec.rb +254 -0
  40. data/spec/faraday/request_spec.rb +0 -11
  41. data/spec/faraday/response/json_spec.rb +117 -0
  42. data/spec/faraday/response/raise_error_spec.rb +7 -4
  43. data/spec/faraday/utils_spec.rb +1 -1
  44. data/spec/support/fake_safe_buffer.rb +1 -1
  45. data/spec/support/shared_examples/request_method.rb +5 -5
  46. metadata +26 -151
  47. data/lib/faraday/adapter/typhoeus.rb +0 -15
  48. data/lib/faraday/autoload.rb +0 -87
  49. data/lib/faraday/dependency_loader.rb +0 -37
  50. data/lib/faraday/request/basic_authentication.rb +0 -20
  51. data/lib/faraday/request/token_authentication.rb +0 -20
  52. data/spec/faraday/adapter/em_http_spec.rb +0 -49
  53. data/spec/faraday/adapter/em_synchrony_spec.rb +0 -18
  54. data/spec/faraday/adapter/excon_spec.rb +0 -49
  55. data/spec/faraday/adapter/httpclient_spec.rb +0 -73
  56. data/spec/faraday/adapter/net_http_spec.rb +0 -64
  57. data/spec/faraday/adapter/patron_spec.rb +0 -18
  58. data/spec/faraday/adapter/rack_spec.rb +0 -8
  59. data/spec/faraday/adapter/typhoeus_spec.rb +0 -7
  60. data/spec/faraday/response/middleware_spec.rb +0 -68
  61. data/spec/support/webmock_rack_app.rb +0 -68
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Faraday::Request::Retry do
4
+ let(:calls) { [] }
5
+ let(:times_called) { calls.size }
6
+ let(:options) { [] }
7
+ let(:conn) do
8
+ Faraday.new do |b|
9
+ b.request :retry, *options
10
+
11
+ b.adapter :test do |stub|
12
+ %w[get post].each do |method|
13
+ stub.send(method, '/unstable') do |env|
14
+ calls << env.dup
15
+ env[:body] = nil # simulate blanking out response body
16
+ callback.call
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ context 'when an unexpected error happens' do
24
+ let(:callback) { -> { raise 'boom!' } }
25
+
26
+ before { expect { conn.get('/unstable') }.to raise_error(RuntimeError) }
27
+
28
+ it { expect(times_called).to eq(1) }
29
+
30
+ context 'and this is passed as a custom exception' do
31
+ let(:options) { [{ exceptions: StandardError }] }
32
+
33
+ it { expect(times_called).to eq(3) }
34
+ end
35
+
36
+ context 'and this is passed as a string custom exception' do
37
+ let(:options) { [{ exceptions: 'StandardError' }] }
38
+
39
+ it { expect(times_called).to eq(3) }
40
+ end
41
+
42
+ context 'and a non-existent string custom exception is passed' do
43
+ let(:options) { [{ exceptions: 'WrongStandardErrorNotExisting' }] }
44
+
45
+ it { expect(times_called).to eq(1) }
46
+ end
47
+ end
48
+
49
+ context 'when an expected error happens' do
50
+ let(:callback) { -> { raise Errno::ETIMEDOUT } }
51
+
52
+ before do
53
+ @started = Time.now
54
+ expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT)
55
+ end
56
+
57
+ it { expect(times_called).to eq(3) }
58
+
59
+ context 'and legacy max_retry set to 1' do
60
+ let(:options) { [1] }
61
+
62
+ it { expect(times_called).to eq(2) }
63
+ end
64
+
65
+ context 'and legacy max_retry set to -9' do
66
+ let(:options) { [-9] }
67
+
68
+ it { expect(times_called).to eq(1) }
69
+ end
70
+
71
+ context 'and new max_retry set to 3' do
72
+ let(:options) { [{ max: 3 }] }
73
+
74
+ it { expect(times_called).to eq(4) }
75
+ end
76
+
77
+ context 'and new max_retry set to -9' do
78
+ let(:options) { [{ max: -9 }] }
79
+
80
+ it { expect(times_called).to eq(1) }
81
+ end
82
+
83
+ context 'and both max_retry and interval are set' do
84
+ let(:options) { [{ max: 2, interval: 0.1 }] }
85
+
86
+ it { expect(Time.now - @started).to be_within(0.04).of(0.2) }
87
+ end
88
+ end
89
+
90
+ context 'when no exception raised' do
91
+ let(:options) { [{ max: 1, retry_statuses: 429 }] }
92
+
93
+ before { conn.get('/unstable') }
94
+
95
+ context 'and response code is in retry_statuses' do
96
+ let(:callback) { -> { [429, {}, ''] } }
97
+
98
+ it { expect(times_called).to eq(2) }
99
+ end
100
+
101
+ context 'and response code is not in retry_statuses' do
102
+ let(:callback) { -> { [503, {}, ''] } }
103
+
104
+ it { expect(times_called).to eq(1) }
105
+ end
106
+ end
107
+
108
+ describe '#calculate_retry_interval' do
109
+ context 'with exponential backoff' do
110
+ let(:options) { { max: 5, interval: 0.1, backoff_factor: 2 } }
111
+ let(:middleware) { Faraday::Request::Retry.new(nil, options) }
112
+
113
+ it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) }
114
+ it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) }
115
+ it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.4) }
116
+ end
117
+
118
+ context 'with exponential backoff and max_interval' do
119
+ let(:options) { { max: 5, interval: 0.1, backoff_factor: 2, max_interval: 0.3 } }
120
+ let(:middleware) { Faraday::Request::Retry.new(nil, options) }
121
+
122
+ it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) }
123
+ it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) }
124
+ it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.3) }
125
+ it { expect(middleware.send(:calculate_retry_interval, 2)).to eq(0.3) }
126
+ end
127
+
128
+ context 'with exponential backoff and interval_randomness' do
129
+ let(:options) { { max: 2, interval: 0.1, interval_randomness: 0.05 } }
130
+ let(:middleware) { Faraday::Request::Retry.new(nil, options) }
131
+
132
+ it { expect(middleware.send(:calculate_retry_interval, 2)).to be_between(0.1, 0.105) }
133
+ end
134
+ end
135
+
136
+ context 'when method is not idempotent' do
137
+ let(:callback) { -> { raise Errno::ETIMEDOUT } }
138
+
139
+ before { expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) }
140
+
141
+ it { expect(times_called).to eq(1) }
142
+ end
143
+
144
+ describe 'retry_if option' do
145
+ let(:callback) { -> { raise Errno::ETIMEDOUT } }
146
+ let(:options) { [{ retry_if: @check }] }
147
+
148
+ it 'retries if retry_if block always returns true' do
149
+ body = { foo: :bar }
150
+ @check = ->(_, _) { true }
151
+ expect { conn.post('/unstable', body) }.to raise_error(Errno::ETIMEDOUT)
152
+ expect(times_called).to eq(3)
153
+ expect(calls.all? { |env| env[:body] == body }).to be_truthy
154
+ end
155
+
156
+ it 'does not retry if retry_if block returns false checking env' do
157
+ @check = ->(env, _) { env[:method] != :post }
158
+ expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT)
159
+ expect(times_called).to eq(1)
160
+ end
161
+
162
+ it 'does not retry if retry_if block returns false checking exception' do
163
+ @check = ->(_, exception) { !exception.is_a?(Errno::ETIMEDOUT) }
164
+ expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT)
165
+ expect(times_called).to eq(1)
166
+ end
167
+
168
+ it 'FilePart: should rewind files on retry' do
169
+ io = StringIO.new('Test data')
170
+ filepart = Faraday::FilePart.new(io, 'application/octet/stream')
171
+
172
+ rewound = 0
173
+ rewind = -> { rewound += 1 }
174
+
175
+ @check = ->(_, _) { true }
176
+ allow(filepart).to receive(:rewind, &rewind)
177
+ expect { conn.post('/unstable', file: filepart) }.to raise_error(Errno::ETIMEDOUT)
178
+ expect(times_called).to eq(3)
179
+ expect(rewound).to eq(2)
180
+ end
181
+
182
+ it 'UploadIO: should rewind files on retry' do
183
+ io = StringIO.new('Test data')
184
+ upload_io = Faraday::FilePart.new(io, 'application/octet/stream')
185
+
186
+ rewound = 0
187
+ rewind = -> { rewound += 1 }
188
+
189
+ @check = ->(_, _) { true }
190
+ allow(upload_io).to receive(:rewind, &rewind)
191
+ expect { conn.post('/unstable', file: upload_io) }.to raise_error(Errno::ETIMEDOUT)
192
+ expect(times_called).to eq(3)
193
+ expect(rewound).to eq(2)
194
+ end
195
+
196
+ context 'when explicitly specifying methods to retry' do
197
+ let(:options) { [{ retry_if: @check, methods: [:post] }] }
198
+
199
+ it 'does not call retry_if for specified methods' do
200
+ @check = ->(_, _) { raise 'this should have never been called' }
201
+ expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT)
202
+ expect(times_called).to eq(3)
203
+ end
204
+ end
205
+
206
+ context 'with empty list of methods to retry' do
207
+ let(:options) { [{ retry_if: @check, methods: [] }] }
208
+
209
+ it 'calls retry_if for all methods' do
210
+ @check = ->(_, _) { calls.size < 2 }
211
+ expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT)
212
+ expect(times_called).to eq(2)
213
+ end
214
+ end
215
+ end
216
+
217
+ describe 'retry_after header support' do
218
+ let(:callback) { -> { [504, headers, ''] } }
219
+ let(:elapsed) { Time.now - @started }
220
+
221
+ before do
222
+ @started = Time.now
223
+ conn.get('/unstable')
224
+ end
225
+
226
+ context 'when retry_after bigger than interval' do
227
+ let(:headers) { { 'Retry-After' => '0.5' } }
228
+ let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] }
229
+
230
+ it { expect(elapsed).to be > 0.5 }
231
+ end
232
+
233
+ context 'when retry_after smaller than interval' do
234
+ let(:headers) { { 'Retry-After' => '0.1' } }
235
+ let(:options) { [{ max: 1, interval: 0.2, retry_statuses: 504 }] }
236
+
237
+ it { expect(elapsed).to be > 0.2 }
238
+ end
239
+
240
+ context 'when retry_after is a timestamp' do
241
+ let(:headers) { { 'Retry-After' => (Time.now.utc + 2).strftime('%a, %d %b %Y %H:%M:%S GMT') } }
242
+ let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] }
243
+
244
+ it { expect(elapsed).to be > 1 }
245
+ end
246
+
247
+ context 'when retry_after is bigger than max_interval' do
248
+ let(:headers) { { 'Retry-After' => (Time.now.utc + 20).strftime('%a, %d %b %Y %H:%M:%S GMT') } }
249
+ let(:options) { [{ max: 2, interval: 0.1, max_interval: 5, retry_statuses: 504 }] }
250
+
251
+ it { expect(times_called).to eq(1) }
252
+ end
253
+ end
254
+ end
@@ -22,17 +22,6 @@ RSpec.describe Faraday::Request do
22
22
  it { expect(subject.http_method).to eq(:post) }
23
23
  end
24
24
 
25
- describe 'deprecate method for HTTP method' do
26
- let(:http_method) { :post }
27
- let(:expected_warning) do
28
- %r{WARNING: `Faraday::Request#method` is deprecated; use `#http_method` instead. It will be removed in or after version 2.0.\n`Faraday::Request#method` called from .+/spec/faraday/request_spec.rb:\d+.}
29
- end
30
-
31
- it { expect(subject.method).to eq(:post) }
32
-
33
- it { expect { subject.method }.to output(expected_warning).to_stderr }
34
- end
35
-
36
25
  context 'when setting the url on setup with a URI' do
37
26
  let(:block) { proc { |req| req.url URI.parse('foo.json?a=1') } }
38
27
 
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Faraday::Response::Json, type: :response do
4
+ let(:options) { {} }
5
+ let(:headers) { {} }
6
+ let(:middleware) do
7
+ described_class.new(lambda { |env|
8
+ Faraday::Response.new(env)
9
+ }, **options)
10
+ end
11
+
12
+ def process(body, content_type = 'application/json', options = {})
13
+ env = {
14
+ body: body, request: options,
15
+ request_headers: Faraday::Utils::Headers.new,
16
+ response_headers: Faraday::Utils::Headers.new(headers)
17
+ }
18
+ env[:response_headers]['content-type'] = content_type if content_type
19
+ yield(env) if block_given?
20
+ middleware.call(Faraday::Env.from(env))
21
+ end
22
+
23
+ context 'no type matching' do
24
+ it "doesn't change nil body" do
25
+ expect(process(nil).body).to be_nil
26
+ end
27
+
28
+ it 'nullifies empty body' do
29
+ expect(process('').body).to be_nil
30
+ end
31
+
32
+ it 'parses json body' do
33
+ response = process('{"a":1}')
34
+ expect(response.body).to eq('a' => 1)
35
+ expect(response.env[:raw_body]).to be_nil
36
+ end
37
+ end
38
+
39
+ context 'with preserving raw' do
40
+ let(:options) { { preserve_raw: true } }
41
+
42
+ it 'parses json body' do
43
+ response = process('{"a":1}')
44
+ expect(response.body).to eq('a' => 1)
45
+ expect(response.env[:raw_body]).to eq('{"a":1}')
46
+ end
47
+ end
48
+
49
+ context 'with default regexp type matching' do
50
+ it 'parses json body of correct type' do
51
+ response = process('{"a":1}', 'application/x-json')
52
+ expect(response.body).to eq('a' => 1)
53
+ end
54
+
55
+ it 'ignores json body of incorrect type' do
56
+ response = process('{"a":1}', 'text/json-xml')
57
+ expect(response.body).to eq('{"a":1}')
58
+ end
59
+ end
60
+
61
+ context 'with array type matching' do
62
+ let(:options) { { content_type: %w[a/b c/d] } }
63
+
64
+ it 'parses json body of correct type' do
65
+ expect(process('{"a":1}', 'a/b').body).to be_a(Hash)
66
+ expect(process('{"a":1}', 'c/d').body).to be_a(Hash)
67
+ end
68
+
69
+ it 'ignores json body of incorrect type' do
70
+ expect(process('{"a":1}', 'a/d').body).not_to be_a(Hash)
71
+ end
72
+ end
73
+
74
+ it 'chokes on invalid json' do
75
+ expect { process('{!') }.to raise_error(Faraday::ParsingError)
76
+ end
77
+
78
+ it 'includes the response on the ParsingError instance' do
79
+ process('{') { |env| env[:response] = Faraday::Response.new }
80
+ raise 'Parsing should have failed.'
81
+ rescue Faraday::ParsingError => e
82
+ expect(e.response).to be_a(Faraday::Response)
83
+ end
84
+
85
+ context 'HEAD responses' do
86
+ it "nullifies the body if it's only one space" do
87
+ response = process(' ')
88
+ expect(response.body).to be_nil
89
+ end
90
+
91
+ it "nullifies the body if it's two spaces" do
92
+ response = process(' ')
93
+ expect(response.body).to be_nil
94
+ end
95
+ end
96
+
97
+ context 'JSON options' do
98
+ let(:body) { '{"a": 1}' }
99
+ let(:result) { { a: 1 } }
100
+ let(:options) do
101
+ {
102
+ parser_options: {
103
+ symbolize_names: true
104
+ }
105
+ }
106
+ end
107
+
108
+ it 'passes relevant options to JSON parse' do
109
+ expect(::JSON).to receive(:parse)
110
+ .with(body, options[:parser_options])
111
+ .and_return(result)
112
+
113
+ response = process(body)
114
+ expect(response.body).to eq(result)
115
+ end
116
+ end
117
+ end
@@ -139,7 +139,7 @@ RSpec.describe Faraday::Response::RaiseError do
139
139
  Faraday.new do |b|
140
140
  b.response :raise_error
141
141
  b.adapter :test do |stub|
142
- stub.post('request?full=true', request_body, request_headers) do
142
+ stub.post(url, request_body, request_headers) do
143
143
  [400, { 'X-Reason' => 'because' }, 'keep looking']
144
144
  end
145
145
  end
@@ -147,11 +147,13 @@ RSpec.describe Faraday::Response::RaiseError do
147
147
  end
148
148
  let(:request_body) { JSON.generate({ 'item' => 'sth' }) }
149
149
  let(:request_headers) { { 'Authorization' => 'Basic 123' } }
150
+ let(:url_path) { 'request' }
151
+ let(:query_params) { 'full=true' }
152
+ let(:url) { "#{url_path}?#{query_params}" }
150
153
 
151
154
  subject(:perform_request) do
152
- conn.post 'request' do |req|
155
+ conn.post url do |req|
153
156
  req.headers['Authorization'] = 'Basic 123'
154
- req.params[:full] = true
155
157
  req.body = request_body
156
158
  end
157
159
  end
@@ -159,7 +161,8 @@ RSpec.describe Faraday::Response::RaiseError do
159
161
  it 'returns the request info in the exception' do
160
162
  expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
161
163
  expect(ex.response[:request][:method]).to eq(:post)
162
- expect(ex.response[:request][:url_path]).to eq('/request')
164
+ expect(ex.response[:request][:url]).to eq(URI("http:/#{url}"))
165
+ expect(ex.response[:request][:url_path]).to eq("/#{url_path}")
163
166
  expect(ex.response[:request][:params]).to eq({ 'full' => 'true' })
164
167
  expect(ex.response[:request][:headers]).to match(a_hash_including(request_headers))
165
168
  expect(ex.response[:request][:body]).to eq(request_body)
@@ -4,7 +4,7 @@ RSpec.describe Faraday::Utils do
4
4
  describe 'headers parsing' do
5
5
  let(:multi_response_headers) do
6
6
  "HTTP/1.x 500 OK\r\nContent-Type: text/html; charset=UTF-8\r\n" \
7
- "HTTP/1.x 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n"
7
+ "HTTP/1.x 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n"
8
8
  end
9
9
 
10
10
  it 'parse headers for aggregated responses' do
@@ -8,7 +8,7 @@ FakeSafeBuffer = Struct.new(:string) do
8
8
 
9
9
  def gsub(regex)
10
10
  string.gsub(regex) do
11
- match, = $&, '' =~ /a/
11
+ match, = Regexp.last_match(0), '' =~ /a/ # rubocop:disable Performance/StringInclude
12
12
  yield(match)
13
13
  end
14
14
  end
@@ -79,7 +79,7 @@ shared_examples 'a request method' do |http_method|
79
79
 
80
80
  on_feature :request_body_on_query_methods do
81
81
  it 'sends request body' do
82
- request_stub.with(Hash[:body, 'test'])
82
+ request_stub.with({ body: 'test' })
83
83
  res = if query_or_body == :body
84
84
  conn.public_send(http_method, '/', 'test')
85
85
  else
@@ -93,7 +93,7 @@ shared_examples 'a request method' do |http_method|
93
93
 
94
94
  it 'sends url encoded parameters' do
95
95
  payload = { name: 'zack' }
96
- request_stub.with(Hash[query_or_body, payload])
96
+ request_stub.with({ query_or_body => payload })
97
97
  res = conn.public_send(http_method, '/', payload)
98
98
  if query_or_body == :query
99
99
  expect(res.env.request_body).to be_nil
@@ -104,7 +104,7 @@ shared_examples 'a request method' do |http_method|
104
104
 
105
105
  it 'sends url encoded nested parameters' do
106
106
  payload = { name: { first: 'zack' } }
107
- request_stub.with(Hash[query_or_body, payload])
107
+ request_stub.with({ query_or_body => payload })
108
108
  conn.public_send(http_method, '/', payload)
109
109
  end
110
110
 
@@ -199,11 +199,11 @@ shared_examples 'a request method' do |http_method|
199
199
  @payload2 = { b: '2' }
200
200
 
201
201
  request_stub
202
- .with(Hash[query_or_body, @payload1])
202
+ .with({ query_or_body => @payload1 })
203
203
  .to_return(body: @payload1.to_json)
204
204
 
205
205
  stub_request(http_method, remote)
206
- .with(Hash[query_or_body, @payload2])
206
+ .with({ query_or_body => @payload2 })
207
207
  .to_return(body: @payload2.to_json)
208
208
 
209
209
  conn.in_parallel do