faraday 0.17.6 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -8
- data/LICENSE.md +1 -1
- data/README.md +18 -358
- data/Rakefile +1 -7
- data/examples/client_spec.rb +65 -0
- data/examples/client_test.rb +79 -0
- data/lib/faraday/adapter/em_http.rb +142 -99
- data/lib/faraday/adapter/em_http_ssl_patch.rb +24 -18
- data/lib/faraday/adapter/em_synchrony/parallel_manager.rb +18 -15
- data/lib/faraday/adapter/em_synchrony.rb +104 -60
- data/lib/faraday/adapter/excon.rb +98 -56
- data/lib/faraday/adapter/httpclient.rb +83 -59
- data/lib/faraday/adapter/net_http.rb +129 -63
- data/lib/faraday/adapter/net_http_persistent.rb +50 -27
- data/lib/faraday/adapter/patron.rb +80 -43
- data/lib/faraday/adapter/rack.rb +30 -13
- data/lib/faraday/adapter/test.rb +86 -53
- data/lib/faraday/adapter/typhoeus.rb +4 -1
- data/lib/faraday/adapter.rb +82 -22
- data/lib/faraday/adapter_registry.rb +30 -0
- data/lib/faraday/autoload.rb +47 -36
- data/lib/faraday/connection.rb +312 -182
- data/lib/faraday/dependency_loader.rb +37 -0
- data/lib/faraday/encoders/flat_params_encoder.rb +98 -0
- data/lib/faraday/encoders/nested_params_encoder.rb +171 -0
- data/lib/faraday/error.rb +9 -35
- data/lib/faraday/file_part.rb +128 -0
- data/lib/faraday/logging/formatter.rb +105 -0
- data/lib/faraday/middleware.rb +12 -28
- data/lib/faraday/middleware_registry.rb +129 -0
- data/lib/faraday/options/connection_options.rb +22 -0
- data/lib/faraday/options/env.rb +181 -0
- data/lib/faraday/options/proxy_options.rb +28 -0
- data/lib/faraday/options/request_options.rb +22 -0
- data/lib/faraday/options/ssl_options.rb +59 -0
- data/lib/faraday/options.rb +32 -183
- data/lib/faraday/param_part.rb +53 -0
- data/lib/faraday/parameters.rb +4 -197
- data/lib/faraday/rack_builder.rb +66 -55
- data/lib/faraday/request/authorization.rb +44 -30
- data/lib/faraday/request/basic_authentication.rb +14 -7
- data/lib/faraday/request/instrumentation.rb +45 -27
- data/lib/faraday/request/multipart.rb +79 -48
- data/lib/faraday/request/retry.rb +197 -171
- data/lib/faraday/request/token_authentication.rb +15 -10
- data/lib/faraday/request/url_encoded.rb +43 -23
- data/lib/faraday/request.rb +68 -38
- data/lib/faraday/response/logger.rb +22 -69
- data/lib/faraday/response/raise_error.rb +38 -18
- data/lib/faraday/response.rb +24 -14
- data/lib/faraday/utils/headers.rb +139 -0
- data/lib/faraday/utils/params_hash.rb +61 -0
- data/lib/faraday/utils.rb +36 -245
- data/lib/faraday.rb +94 -175
- data/spec/external_adapters/faraday_specs_setup.rb +14 -0
- data/spec/faraday/adapter/em_http_spec.rb +47 -0
- data/spec/faraday/adapter/em_synchrony_spec.rb +16 -0
- data/spec/faraday/adapter/excon_spec.rb +49 -0
- data/spec/faraday/adapter/httpclient_spec.rb +73 -0
- data/spec/faraday/adapter/net_http_persistent_spec.rb +57 -0
- data/spec/faraday/adapter/net_http_spec.rb +64 -0
- data/spec/faraday/adapter/patron_spec.rb +18 -0
- data/spec/faraday/adapter/rack_spec.rb +8 -0
- data/spec/faraday/adapter/typhoeus_spec.rb +7 -0
- data/spec/faraday/adapter_registry_spec.rb +28 -0
- data/spec/faraday/adapter_spec.rb +55 -0
- data/spec/faraday/composite_read_io_spec.rb +80 -0
- data/spec/faraday/connection_spec.rb +691 -0
- data/spec/faraday/error_spec.rb +0 -57
- data/spec/faraday/middleware_spec.rb +26 -0
- data/spec/faraday/options/env_spec.rb +70 -0
- data/spec/faraday/options/options_spec.rb +297 -0
- data/spec/faraday/options/proxy_options_spec.rb +37 -0
- data/spec/faraday/options/request_options_spec.rb +19 -0
- data/spec/faraday/params_encoders/flat_spec.rb +34 -0
- data/spec/faraday/params_encoders/nested_spec.rb +134 -0
- data/spec/faraday/rack_builder_spec.rb +196 -0
- data/spec/faraday/request/authorization_spec.rb +88 -0
- data/spec/faraday/request/instrumentation_spec.rb +76 -0
- data/spec/faraday/request/multipart_spec.rb +274 -0
- data/spec/faraday/request/retry_spec.rb +242 -0
- data/spec/faraday/request/url_encoded_spec.rb +83 -0
- data/spec/faraday/request_spec.rb +109 -0
- data/spec/faraday/response/logger_spec.rb +220 -0
- data/spec/faraday/response/middleware_spec.rb +68 -0
- data/spec/faraday/response/raise_error_spec.rb +15 -15
- data/spec/faraday/response_spec.rb +75 -0
- data/spec/faraday/utils/headers_spec.rb +82 -0
- data/spec/faraday/utils_spec.rb +56 -0
- data/spec/faraday_spec.rb +37 -0
- data/spec/spec_helper.rb +63 -36
- data/spec/support/disabling_stub.rb +14 -0
- data/spec/support/fake_safe_buffer.rb +15 -0
- data/spec/support/helper_methods.rb +133 -0
- data/spec/support/shared_examples/adapter.rb +104 -0
- data/spec/support/shared_examples/params_encoder.rb +18 -0
- data/spec/support/shared_examples/request_method.rb +234 -0
- data/spec/support/streaming_response_checker.rb +35 -0
- data/spec/support/webmock_rack_app.rb +68 -0
- metadata +66 -38
- data/lib/faraday/deprecate.rb +0 -109
- data/lib/faraday/upload_io.rb +0 -77
- data/spec/faraday/deprecate_spec.rb +0 -147
- data/test/adapters/default_test.rb +0 -14
- data/test/adapters/em_http_test.rb +0 -30
- data/test/adapters/em_synchrony_test.rb +0 -32
- data/test/adapters/excon_test.rb +0 -30
- data/test/adapters/httpclient_test.rb +0 -34
- data/test/adapters/integration.rb +0 -263
- data/test/adapters/logger_test.rb +0 -136
- data/test/adapters/net_http_persistent_test.rb +0 -114
- data/test/adapters/net_http_test.rb +0 -79
- data/test/adapters/patron_test.rb +0 -40
- data/test/adapters/rack_test.rb +0 -38
- data/test/adapters/test_middleware_test.rb +0 -157
- data/test/adapters/typhoeus_test.rb +0 -38
- data/test/authentication_middleware_test.rb +0 -65
- data/test/composite_read_io_test.rb +0 -109
- data/test/connection_test.rb +0 -738
- data/test/env_test.rb +0 -268
- data/test/helper.rb +0 -75
- data/test/live_server.rb +0 -67
- data/test/middleware/instrumentation_test.rb +0 -88
- data/test/middleware/retry_test.rb +0 -282
- data/test/middleware_stack_test.rb +0 -260
- data/test/multibyte.txt +0 -1
- data/test/options_test.rb +0 -333
- data/test/parameters_test.rb +0 -157
- data/test/request_middleware_test.rb +0 -126
- data/test/response_middleware_test.rb +0 -72
- data/test/strawberry.rb +0 -2
- data/test/utils_test.rb +0 -98
@@ -0,0 +1,242 @@
|
|
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
|
+
end
|
36
|
+
|
37
|
+
context 'when an expected error happens' do
|
38
|
+
let(:callback) { -> { raise Errno::ETIMEDOUT } }
|
39
|
+
|
40
|
+
before do
|
41
|
+
@started = Time.now
|
42
|
+
expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT)
|
43
|
+
end
|
44
|
+
|
45
|
+
it { expect(times_called).to eq(3) }
|
46
|
+
|
47
|
+
context 'and legacy max_retry set to 1' do
|
48
|
+
let(:options) { [1] }
|
49
|
+
|
50
|
+
it { expect(times_called).to eq(2) }
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'and legacy max_retry set to -9' do
|
54
|
+
let(:options) { [-9] }
|
55
|
+
|
56
|
+
it { expect(times_called).to eq(1) }
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'and new max_retry set to 3' do
|
60
|
+
let(:options) { [{ max: 3 }] }
|
61
|
+
|
62
|
+
it { expect(times_called).to eq(4) }
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'and new max_retry set to -9' do
|
66
|
+
let(:options) { [{ max: -9 }] }
|
67
|
+
|
68
|
+
it { expect(times_called).to eq(1) }
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'and both max_retry and interval are set' do
|
72
|
+
let(:options) { [{ max: 2, interval: 0.1 }] }
|
73
|
+
|
74
|
+
it { expect(Time.now - @started).to be_within(0.04).of(0.2) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'when no exception raised' do
|
79
|
+
let(:options) { [{ max: 1, retry_statuses: 429 }] }
|
80
|
+
|
81
|
+
before { conn.get('/unstable') }
|
82
|
+
|
83
|
+
context 'and response code is in retry_statuses' do
|
84
|
+
let(:callback) { -> { [429, {}, ''] } }
|
85
|
+
|
86
|
+
it { expect(times_called).to eq(2) }
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'and response code is not in retry_statuses' do
|
90
|
+
let(:callback) { -> { [503, {}, ''] } }
|
91
|
+
|
92
|
+
it { expect(times_called).to eq(1) }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#calculate_retry_interval' do
|
97
|
+
context 'with exponential backoff' do
|
98
|
+
let(:options) { { max: 5, interval: 0.1, backoff_factor: 2 } }
|
99
|
+
let(:middleware) { Faraday::Request::Retry.new(nil, options) }
|
100
|
+
|
101
|
+
it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) }
|
102
|
+
it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) }
|
103
|
+
it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.4) }
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'with exponential backoff and max_interval' do
|
107
|
+
let(:options) { { max: 5, interval: 0.1, backoff_factor: 2, max_interval: 0.3 } }
|
108
|
+
let(:middleware) { Faraday::Request::Retry.new(nil, options) }
|
109
|
+
|
110
|
+
it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) }
|
111
|
+
it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) }
|
112
|
+
it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.3) }
|
113
|
+
it { expect(middleware.send(:calculate_retry_interval, 2)).to eq(0.3) }
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'with exponential backoff and interval_randomness' do
|
117
|
+
let(:options) { { max: 2, interval: 0.1, interval_randomness: 0.05 } }
|
118
|
+
let(:middleware) { Faraday::Request::Retry.new(nil, options) }
|
119
|
+
|
120
|
+
it { expect(middleware.send(:calculate_retry_interval, 2)).to be_between(0.1, 0.15) }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context 'when method is not idempotent' do
|
125
|
+
let(:callback) { -> { raise Errno::ETIMEDOUT } }
|
126
|
+
|
127
|
+
before { expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) }
|
128
|
+
|
129
|
+
it { expect(times_called).to eq(1) }
|
130
|
+
end
|
131
|
+
|
132
|
+
describe 'retry_if option' do
|
133
|
+
let(:callback) { -> { raise Errno::ETIMEDOUT } }
|
134
|
+
let(:options) { [{ retry_if: @check }] }
|
135
|
+
|
136
|
+
it 'retries if retry_if block always returns true' do
|
137
|
+
body = { foo: :bar }
|
138
|
+
@check = ->(_, _) { true }
|
139
|
+
expect { conn.post('/unstable', body) }.to raise_error(Errno::ETIMEDOUT)
|
140
|
+
expect(times_called).to eq(3)
|
141
|
+
expect(calls.all? { |env| env[:body] == body }).to be_truthy
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'does not retry if retry_if block returns false checking env' do
|
145
|
+
@check = ->(env, _) { env[:method] != :post }
|
146
|
+
expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT)
|
147
|
+
expect(times_called).to eq(1)
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'does not retry if retry_if block returns false checking exception' do
|
151
|
+
@check = ->(_, exception) { !exception.is_a?(Errno::ETIMEDOUT) }
|
152
|
+
expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT)
|
153
|
+
expect(times_called).to eq(1)
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'FilePart: should rewind files on retry' do
|
157
|
+
io = StringIO.new('Test data')
|
158
|
+
filepart = Faraday::FilePart.new(io, 'application/octet/stream')
|
159
|
+
|
160
|
+
rewound = 0
|
161
|
+
rewind = -> { rewound += 1 }
|
162
|
+
|
163
|
+
@check = ->(_, _) { true }
|
164
|
+
allow(filepart).to receive(:rewind, &rewind)
|
165
|
+
expect { conn.post('/unstable', file: filepart) }.to raise_error(Errno::ETIMEDOUT)
|
166
|
+
expect(times_called).to eq(3)
|
167
|
+
expect(rewound).to eq(2)
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'UploadIO: should rewind files on retry' do
|
171
|
+
io = StringIO.new('Test data')
|
172
|
+
upload_io = Faraday::UploadIO.new(io, 'application/octet/stream')
|
173
|
+
|
174
|
+
rewound = 0
|
175
|
+
rewind = -> { rewound += 1 }
|
176
|
+
|
177
|
+
@check = ->(_, _) { true }
|
178
|
+
allow(upload_io).to receive(:rewind, &rewind)
|
179
|
+
expect { conn.post('/unstable', file: upload_io) }.to raise_error(Errno::ETIMEDOUT)
|
180
|
+
expect(times_called).to eq(3)
|
181
|
+
expect(rewound).to eq(2)
|
182
|
+
end
|
183
|
+
|
184
|
+
context 'when explicitly specifying methods to retry' do
|
185
|
+
let(:options) { [{ retry_if: @check, methods: [:post] }] }
|
186
|
+
|
187
|
+
it 'does not call retry_if for specified methods' do
|
188
|
+
@check = ->(_, _) { raise 'this should have never been called' }
|
189
|
+
expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT)
|
190
|
+
expect(times_called).to eq(3)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
context 'with empty list of methods to retry' do
|
195
|
+
let(:options) { [{ retry_if: @check, methods: [] }] }
|
196
|
+
|
197
|
+
it 'calls retry_if for all methods' do
|
198
|
+
@check = ->(_, _) { calls.size < 2 }
|
199
|
+
expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT)
|
200
|
+
expect(times_called).to eq(2)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe 'retry_after header support' do
|
206
|
+
let(:callback) { -> { [504, headers, ''] } }
|
207
|
+
let(:elapsed) { Time.now - @started }
|
208
|
+
|
209
|
+
before do
|
210
|
+
@started = Time.now
|
211
|
+
conn.get('/unstable')
|
212
|
+
end
|
213
|
+
|
214
|
+
context 'when retry_after bigger than interval' do
|
215
|
+
let(:headers) { { 'Retry-After' => '0.5' } }
|
216
|
+
let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] }
|
217
|
+
|
218
|
+
it { expect(elapsed).to be > 0.5 }
|
219
|
+
end
|
220
|
+
|
221
|
+
context 'when retry_after smaller than interval' do
|
222
|
+
let(:headers) { { 'Retry-After' => '0.1' } }
|
223
|
+
let(:options) { [{ max: 1, interval: 0.2, retry_statuses: 504 }] }
|
224
|
+
|
225
|
+
it { expect(elapsed).to be > 0.2 }
|
226
|
+
end
|
227
|
+
|
228
|
+
context 'when retry_after is a timestamp' do
|
229
|
+
let(:headers) { { 'Retry-After' => (Time.now.utc + 2).strftime('%a, %d %b %Y %H:%M:%S GMT') } }
|
230
|
+
let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] }
|
231
|
+
|
232
|
+
it { expect(elapsed).to be > 1 }
|
233
|
+
end
|
234
|
+
|
235
|
+
context 'when retry_after is bigger than max_interval' do
|
236
|
+
let(:headers) { { 'Retry-After' => (Time.now.utc + 20).strftime('%a, %d %b %Y %H:%M:%S GMT') } }
|
237
|
+
let(:options) { [{ max: 2, interval: 0.1, max_interval: 5, retry_statuses: 504 }] }
|
238
|
+
|
239
|
+
it { expect(times_called).to eq(1) }
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Faraday::Request::UrlEncoded do
|
4
|
+
let(:conn) do
|
5
|
+
Faraday.new do |b|
|
6
|
+
b.request :multipart
|
7
|
+
b.request :url_encoded
|
8
|
+
b.adapter :test do |stub|
|
9
|
+
stub.post('/echo') do |env|
|
10
|
+
posted_as = env[:request_headers]['Content-Type']
|
11
|
+
[200, { 'Content-Type' => posted_as }, env[:body]]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'does nothing without payload' do
|
18
|
+
response = conn.post('/echo')
|
19
|
+
expect(response.headers['Content-Type']).to be_nil
|
20
|
+
expect(response.body.empty?).to be_truthy
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'ignores custom content type' do
|
24
|
+
response = conn.post('/echo', { some: 'data' }, 'content-type' => 'application/x-foo')
|
25
|
+
expect(response.headers['Content-Type']).to eq('application/x-foo')
|
26
|
+
expect(response.body).to eq(some: 'data')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'works with no headers' do
|
30
|
+
response = conn.post('/echo', fruit: %w[apples oranges])
|
31
|
+
expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
|
32
|
+
expect(response.body).to eq('fruit%5B%5D=apples&fruit%5B%5D=oranges')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'works with with headers' do
|
36
|
+
response = conn.post('/echo', { 'a' => 123 }, 'content-type' => 'application/x-www-form-urlencoded')
|
37
|
+
expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
|
38
|
+
expect(response.body).to eq('a=123')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'works with nested params' do
|
42
|
+
response = conn.post('/echo', user: { name: 'Mislav', web: 'mislav.net' })
|
43
|
+
expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
|
44
|
+
expected = { 'user' => { 'name' => 'Mislav', 'web' => 'mislav.net' } }
|
45
|
+
expect(Faraday::Utils.parse_nested_query(response.body)).to eq(expected)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'works with non nested params' do
|
49
|
+
response = conn.post('/echo', dimensions: %w[date location]) do |req|
|
50
|
+
req.options.params_encoder = Faraday::FlatParamsEncoder
|
51
|
+
end
|
52
|
+
expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
|
53
|
+
expected = { 'dimensions' => %w[date location] }
|
54
|
+
expect(Faraday::Utils.parse_query(response.body)).to eq(expected)
|
55
|
+
expect(response.body).to eq('dimensions=date&dimensions=location')
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'works with unicode' do
|
59
|
+
err = capture_warnings do
|
60
|
+
response = conn.post('/echo', str: 'eé cç aã aâ')
|
61
|
+
expect(response.body).to eq('str=e%C3%A9+c%C3%A7+a%C3%A3+a%C3%A2')
|
62
|
+
end
|
63
|
+
expect(err.empty?).to be_truthy
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'works with nested keys' do
|
67
|
+
response = conn.post('/echo', 'a' => { 'b' => { 'c' => ['d'] } })
|
68
|
+
expect(response.body).to eq('a%5Bb%5D%5Bc%5D%5B%5D=d')
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'customising default_space_encoding' do
|
72
|
+
around do |example|
|
73
|
+
Faraday::Utils.default_space_encoding = '%20'
|
74
|
+
example.run
|
75
|
+
Faraday::Utils.default_space_encoding = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'uses the custom character to encode spaces' do
|
79
|
+
response = conn.post('/echo', str: 'apple banana')
|
80
|
+
expect(response.body).to eq('str=apple%20banana')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Faraday::Request do
|
4
|
+
let(:conn) do
|
5
|
+
Faraday.new(url: 'http://sushi.com/api',
|
6
|
+
headers: { 'Mime-Version' => '1.0' },
|
7
|
+
request: { oauth: { consumer_key: 'anonymous' } })
|
8
|
+
end
|
9
|
+
let(:method) { :get }
|
10
|
+
let(:block) { nil }
|
11
|
+
|
12
|
+
subject { conn.build_request(method, &block) }
|
13
|
+
|
14
|
+
context 'when nothing particular is configured' do
|
15
|
+
it { expect(subject.method).to eq(:get) }
|
16
|
+
it { expect(subject.to_env(conn).ssl.verify).to be_falsey }
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'when method is post' do
|
20
|
+
let(:method) { :post }
|
21
|
+
|
22
|
+
it { expect(subject.method).to eq(:post) }
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'when setting the url on setup with a URI' do
|
26
|
+
let(:block) { proc { |req| req.url URI.parse('foo.json?a=1') } }
|
27
|
+
|
28
|
+
it { expect(subject.path).to eq(URI.parse('foo.json')) }
|
29
|
+
it { expect(subject.params).to eq('a' => '1') }
|
30
|
+
it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1') }
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when setting the url on setup with a string path and params' do
|
34
|
+
let(:block) { proc { |req| req.url 'foo.json', 'a' => 1 } }
|
35
|
+
|
36
|
+
it { expect(subject.path).to eq('foo.json') }
|
37
|
+
it { expect(subject.params).to eq('a' => 1) }
|
38
|
+
it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1') }
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'when setting the url on setup with a path including params' do
|
42
|
+
let(:block) { proc { |req| req.url 'foo.json?b=2&a=1#qqq' } }
|
43
|
+
|
44
|
+
it { expect(subject.path).to eq('foo.json') }
|
45
|
+
it { expect(subject.params).to eq('a' => '1', 'b' => '2') }
|
46
|
+
it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1&b=2') }
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'when setting a header on setup with []= syntax' do
|
50
|
+
let(:block) { proc { |req| req['Server'] = 'Faraday' } }
|
51
|
+
let(:headers) { subject.to_env(conn).request_headers }
|
52
|
+
|
53
|
+
it { expect(subject.headers['Server']).to eq('Faraday') }
|
54
|
+
it { expect(headers['mime-version']).to eq('1.0') }
|
55
|
+
it { expect(headers['server']).to eq('Faraday') }
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'when setting the body on setup' do
|
59
|
+
let(:block) { proc { |req| req.body = 'hi' } }
|
60
|
+
|
61
|
+
it { expect(subject.body).to eq('hi') }
|
62
|
+
it { expect(subject.to_env(conn).body).to eq('hi') }
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'with global request options set' do
|
66
|
+
let(:env_request) { subject.to_env(conn).request }
|
67
|
+
|
68
|
+
before do
|
69
|
+
conn.options.timeout = 3
|
70
|
+
conn.options.open_timeout = 5
|
71
|
+
conn.ssl.verify = false
|
72
|
+
conn.proxy = 'http://proxy.com'
|
73
|
+
end
|
74
|
+
|
75
|
+
it { expect(subject.options.timeout).to eq(3) }
|
76
|
+
it { expect(subject.options.open_timeout).to eq(5) }
|
77
|
+
it { expect(env_request.timeout).to eq(3) }
|
78
|
+
it { expect(env_request.open_timeout).to eq(5) }
|
79
|
+
|
80
|
+
context 'and per-request options set' do
|
81
|
+
let(:block) do
|
82
|
+
proc do |req|
|
83
|
+
req.options.timeout = 10
|
84
|
+
req.options.boundary = 'boo'
|
85
|
+
req.options.oauth[:consumer_secret] = 'xyz'
|
86
|
+
req.options.context = {
|
87
|
+
foo: 'foo',
|
88
|
+
bar: 'bar'
|
89
|
+
}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it { expect(subject.options.timeout).to eq(10) }
|
94
|
+
it { expect(subject.options.open_timeout).to eq(5) }
|
95
|
+
it { expect(env_request.timeout).to eq(10) }
|
96
|
+
it { expect(env_request.open_timeout).to eq(5) }
|
97
|
+
it { expect(env_request.boundary).to eq('boo') }
|
98
|
+
it { expect(env_request.context).to eq(foo: 'foo', bar: 'bar') }
|
99
|
+
it do
|
100
|
+
oauth_expected = { consumer_secret: 'xyz', consumer_key: 'anonymous' }
|
101
|
+
expect(env_request.oauth).to eq(oauth_expected)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'supports marshal serialization' do
|
107
|
+
expect(Marshal.load(Marshal.dump(subject))).to eq(subject)
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
RSpec.describe Faraday::Response::Logger do
|
7
|
+
let(:string_io) { StringIO.new }
|
8
|
+
let(:logger) { Logger.new(string_io) }
|
9
|
+
let(:logger_options) { {} }
|
10
|
+
let(:conn) do
|
11
|
+
rubbles = ['Barney', 'Betty', 'Bam Bam']
|
12
|
+
|
13
|
+
Faraday.new do |b|
|
14
|
+
b.response :logger, logger, logger_options do |logger|
|
15
|
+
logger.filter(/(soylent green is) (.+)/, '\1 tasty')
|
16
|
+
logger.filter(/(api_key:).*"(.+)."/, '\1[API_KEY]')
|
17
|
+
logger.filter(/(password)=(.+)/, '\1=[HIDDEN]')
|
18
|
+
end
|
19
|
+
b.adapter :test do |stubs|
|
20
|
+
stubs.get('/hello') { [200, { 'Content-Type' => 'text/html' }, 'hello'] }
|
21
|
+
stubs.post('/ohai') { [200, { 'Content-Type' => 'text/html' }, 'fred'] }
|
22
|
+
stubs.post('/ohyes') { [200, { 'Content-Type' => 'text/html' }, 'pebbles'] }
|
23
|
+
stubs.get('/rubbles') { [200, { 'Content-Type' => 'application/json' }, rubbles] }
|
24
|
+
stubs.get('/filtered_body') { [200, { 'Content-Type' => 'text/html' }, 'soylent green is people'] }
|
25
|
+
stubs.get('/filtered_headers') { [200, { 'Content-Type' => 'text/html' }, 'headers response'] }
|
26
|
+
stubs.get('/filtered_params') { [200, { 'Content-Type' => 'text/html' }, 'params response'] }
|
27
|
+
stubs.get('/filtered_url') { [200, { 'Content-Type' => 'text/html' }, 'url response'] }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
before do
|
33
|
+
logger.level = Logger::DEBUG
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'still returns output' do
|
37
|
+
resp = conn.get '/hello', nil, accept: 'text/html'
|
38
|
+
expect(resp.body).to eq('hello')
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'without configuration' do
|
42
|
+
let(:conn) do
|
43
|
+
Faraday.new do |b|
|
44
|
+
b.response :logger
|
45
|
+
b.adapter :test do |stubs|
|
46
|
+
stubs.get('/hello') { [200, { 'Content-Type' => 'text/html' }, 'hello'] }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'defaults to stdout' do
|
52
|
+
expect(Logger).to receive(:new).with($stdout).and_return(Logger.new(nil))
|
53
|
+
conn.get('/hello')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'with default formatter' do
|
58
|
+
let(:formatter) { instance_double(Faraday::Logging::Formatter, request: true, response: true, filter: []) }
|
59
|
+
|
60
|
+
before { allow(Faraday::Logging::Formatter).to receive(:new).and_return(formatter) }
|
61
|
+
|
62
|
+
it 'delegates logging to the formatter' do
|
63
|
+
expect(formatter).to receive(:request).with(an_instance_of(Faraday::Env))
|
64
|
+
expect(formatter).to receive(:response).with(an_instance_of(Faraday::Env))
|
65
|
+
conn.get '/hello'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'with custom formatter' do
|
70
|
+
let(:formatter_class) do
|
71
|
+
Class.new(Faraday::Logging::Formatter) do
|
72
|
+
def request(_env)
|
73
|
+
info 'Custom log formatter request'
|
74
|
+
end
|
75
|
+
|
76
|
+
def response(_env)
|
77
|
+
info 'Custom log formatter response'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
let(:logger_options) { { formatter: formatter_class } }
|
83
|
+
|
84
|
+
it 'logs with custom formatter' do
|
85
|
+
conn.get '/hello'
|
86
|
+
|
87
|
+
expect(string_io.string).to match('Custom log formatter request')
|
88
|
+
expect(string_io.string).to match('Custom log formatter response')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'logs method and url' do
|
93
|
+
conn.get '/hello', nil, accept: 'text/html'
|
94
|
+
expect(string_io.string).to match('GET http:/hello')
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'logs request headers by default' do
|
98
|
+
conn.get '/hello', nil, accept: 'text/html'
|
99
|
+
expect(string_io.string).to match(%(Accept: "text/html))
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'logs response headers by default' do
|
103
|
+
conn.get '/hello', nil, accept: 'text/html'
|
104
|
+
expect(string_io.string).to match(%(Content-Type: "text/html))
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'does not log request body by default' do
|
108
|
+
conn.post '/ohai', 'name=Unagi', accept: 'text/html'
|
109
|
+
expect(string_io.string).not_to match(%(name=Unagi))
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'does not log response body by default' do
|
113
|
+
conn.post '/ohai', 'name=Toro', accept: 'text/html'
|
114
|
+
expect(string_io.string).not_to match(%(fred))
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'logs filter headers' do
|
118
|
+
conn.headers = { 'api_key' => 'ABC123' }
|
119
|
+
conn.get '/filtered_headers', nil, accept: 'text/html'
|
120
|
+
expect(string_io.string).to match(%(api_key:))
|
121
|
+
expect(string_io.string).to match(%([API_KEY]))
|
122
|
+
expect(string_io.string).not_to match(%(ABC123))
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'logs filter url' do
|
126
|
+
conn.get '/filtered_url?password=hunter2', nil, accept: 'text/html'
|
127
|
+
expect(string_io.string).to match(%([HIDDEN]))
|
128
|
+
expect(string_io.string).not_to match(%(hunter2))
|
129
|
+
end
|
130
|
+
|
131
|
+
context 'when not logging request headers' do
|
132
|
+
let(:logger_options) { { headers: { request: false } } }
|
133
|
+
|
134
|
+
it 'does not log request headers if option is false' do
|
135
|
+
conn.get '/hello', nil, accept: 'text/html'
|
136
|
+
expect(string_io.string).not_to match(%(Accept: "text/html))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context 'when not logging response headers' do
|
141
|
+
let(:logger_options) { { headers: { response: false } } }
|
142
|
+
|
143
|
+
it 'does not log response headers if option is false' do
|
144
|
+
conn.get '/hello', nil, accept: 'text/html'
|
145
|
+
expect(string_io.string).not_to match(%(Content-Type: "text/html))
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'when logging request body' do
|
150
|
+
let(:logger_options) { { bodies: { request: true } } }
|
151
|
+
|
152
|
+
it 'log only request body' do
|
153
|
+
conn.post '/ohyes', 'name=Tamago', accept: 'text/html'
|
154
|
+
expect(string_io.string).to match(%(name=Tamago))
|
155
|
+
expect(string_io.string).not_to match(%(pebbles))
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
context 'when logging response body' do
|
160
|
+
let(:logger_options) { { bodies: { response: true } } }
|
161
|
+
|
162
|
+
it 'log only response body' do
|
163
|
+
conn.post '/ohyes', 'name=Hamachi', accept: 'text/html'
|
164
|
+
expect(string_io.string).to match(%(pebbles))
|
165
|
+
expect(string_io.string).not_to match(%(name=Hamachi))
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
context 'when logging request and response bodies' do
|
170
|
+
let(:logger_options) { { bodies: true } }
|
171
|
+
|
172
|
+
it 'log request and response body' do
|
173
|
+
conn.post '/ohyes', 'name=Ebi', accept: 'text/html'
|
174
|
+
expect(string_io.string).to match(%(name=Ebi))
|
175
|
+
expect(string_io.string).to match(%(pebbles))
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'log response body object' do
|
179
|
+
conn.get '/rubbles', nil, accept: 'text/html'
|
180
|
+
expect(string_io.string).to match(%([\"Barney\", \"Betty\", \"Bam Bam\"]\n))
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'logs filter body' do
|
184
|
+
conn.get '/filtered_body', nil, accept: 'text/html'
|
185
|
+
expect(string_io.string).to match(%(soylent green is))
|
186
|
+
expect(string_io.string).to match(%(tasty))
|
187
|
+
expect(string_io.string).not_to match(%(people))
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context 'when using log_level' do
|
192
|
+
let(:logger_options) { { bodies: true, log_level: :debug } }
|
193
|
+
|
194
|
+
it 'logs request/request body on the specified level (debug)' do
|
195
|
+
logger.level = Logger::DEBUG
|
196
|
+
conn.post '/ohyes', 'name=Ebi', accept: 'text/html'
|
197
|
+
expect(string_io.string).to match(%(name=Ebi))
|
198
|
+
expect(string_io.string).to match(%(pebbles))
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'logs headers on the debug level' do
|
202
|
+
logger.level = Logger::DEBUG
|
203
|
+
conn.get '/hello', nil, accept: 'text/html'
|
204
|
+
expect(string_io.string).to match(%(Content-Type: "text/html))
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'does not log request/response body on the info level' do
|
208
|
+
logger.level = Logger::INFO
|
209
|
+
conn.post '/ohyes', 'name=Ebi', accept: 'text/html'
|
210
|
+
expect(string_io.string).not_to match(%(name=Ebi))
|
211
|
+
expect(string_io.string).not_to match(%(pebbles))
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'does not log headers on the info level' do
|
215
|
+
logger.level = Logger::INFO
|
216
|
+
conn.get '/hello', nil, accept: 'text/html'
|
217
|
+
expect(string_io.string).not_to match(%(Content-Type: "text/html))
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|