faraday 2.7.4 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,7 +30,7 @@ module Faraday
30
30
  new_value = value
31
31
  end
32
32
 
33
- send("#{key}=", new_value) unless new_value.nil?
33
+ send(:"#{key}=", new_value) unless new_value.nil?
34
34
  end
35
35
  self
36
36
  end
@@ -38,7 +38,7 @@ module Faraday
38
38
  # Public
39
39
  def delete(key)
40
40
  value = send(key)
41
- send("#{key}=", nil)
41
+ send(:"#{key}=", nil)
42
42
  value
43
43
  end
44
44
 
@@ -57,7 +57,7 @@ module Faraday
57
57
  else
58
58
  other_value
59
59
  end
60
- send("#{key}=", new_value) unless new_value.nil?
60
+ send(:"#{key}=", new_value) unless new_value.nil?
61
61
  end
62
62
  self
63
63
  end
@@ -174,6 +174,7 @@ module Faraday
174
174
 
175
175
  memoized_attributes[key.to_sym] = block
176
176
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
177
+ remove_method(key) if method_defined?(key, false)
177
178
  def #{key}() self[:#{key}]; end
178
179
  RUBY
179
180
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ruby2_keywords'
4
3
  require 'faraday/adapter_registry'
5
4
 
6
5
  module Faraday
@@ -5,12 +5,14 @@ module Faraday
5
5
  # Middleware for instrumenting Requests.
6
6
  class Instrumentation < Faraday::Middleware
7
7
  # Options class used in Request::Instrumentation class.
8
- class Options < Faraday::Options.new(:name, :instrumenter)
8
+ Options = Faraday::Options.new(:name, :instrumenter) do
9
+ remove_method :name
9
10
  # @return [String]
10
11
  def name
11
12
  self[:name] ||= 'request.faraday'
12
13
  end
13
14
 
15
+ remove_method :instrumenter
14
16
  # @return [Class]
15
17
  def instrumenter
16
18
  self[:instrumenter] ||= ActiveSupport::Notifications
@@ -13,7 +13,7 @@ module Faraday
13
13
  # Doesn't try to encode bodies that already are in string form.
14
14
  class Json < Middleware
15
15
  MIME_TYPE = 'application/json'
16
- MIME_TYPE_REGEX = %r{^application/(vnd\..+\+)?json$}.freeze
16
+ MIME_TYPE_REGEX = %r{^application/(vnd\..+\+)?json$}
17
17
 
18
18
  def on_request(env)
19
19
  match_content_type(env) do |data|
@@ -24,7 +24,13 @@ module Faraday
24
24
  private
25
25
 
26
26
  def encode(data)
27
- ::JSON.generate(data)
27
+ if options[:encoder].is_a?(Array) && options[:encoder].size >= 2
28
+ options[:encoder][0].public_send(options[:encoder][1], data)
29
+ elsif options[:encoder].respond_to?(:dump)
30
+ options[:encoder].dump(data)
31
+ else
32
+ ::JSON.generate(data)
33
+ end
28
34
  end
29
35
 
30
36
  def match_content_type(env)
@@ -40,7 +46,16 @@ module Faraday
40
46
  end
41
47
 
42
48
  def body?(env)
43
- (body = env[:body]) && !(body.respond_to?(:to_str) && body.empty?)
49
+ body = env[:body]
50
+ case body
51
+ when true, false
52
+ true
53
+ when nil
54
+ # NOTE: nil can be converted to `"null"`, but this middleware doesn't process `nil` for the compatibility.
55
+ false
56
+ else
57
+ !(body.respond_to?(:to_str) && body.empty?)
58
+ end
44
59
  end
45
60
 
46
61
  def request_type(env)
@@ -24,13 +24,14 @@ module Faraday
24
24
  # @return [String] body
25
25
  # @!attribute options
26
26
  # @return [RequestOptions] options
27
- #
28
- # rubocop:disable Style/StructInheritance
29
- class Request < Struct.new(:http_method, :path, :params, :headers, :body, :options)
30
- # rubocop:enable Style/StructInheritance
31
-
27
+ Request = Struct.new(:http_method, :path, :params, :headers, :body, :options) do
32
28
  extend MiddlewareRegistry
33
29
 
30
+ alias_method :member_get, :[]
31
+ private :member_get
32
+ alias_method :member_set, :[]=
33
+ private :member_set
34
+
34
35
  # @param request_method [String]
35
36
  # @yield [request] for block customization, if block given
36
37
  # @yieldparam request [Request]
@@ -41,6 +42,7 @@ module Faraday
41
42
  end
42
43
  end
43
44
 
45
+ remove_method :params=
44
46
  # Replace params, preserving the existing hash type.
45
47
  #
46
48
  # @param hash [Hash] new params
@@ -48,10 +50,11 @@ module Faraday
48
50
  if params
49
51
  params.replace hash
50
52
  else
51
- super
53
+ member_set(:params, hash)
52
54
  end
53
55
  end
54
56
 
57
+ remove_method :headers=
55
58
  # Replace request headers, preserving the existing hash type.
56
59
  #
57
60
  # @param hash [Hash] new headers
@@ -59,7 +62,7 @@ module Faraday
59
62
  if headers
60
63
  headers.replace hash
61
64
  else
62
- super
65
+ member_set(:headers, hash)
63
66
  end
64
67
  end
65
68
 
@@ -11,6 +11,8 @@ module Faraday
11
11
  @parser_options = parser_options
12
12
  @content_types = Array(content_type)
13
13
  @preserve_raw = preserve_raw
14
+
15
+ process_parser_options
14
16
  end
15
17
 
16
18
  def on_complete(env)
@@ -27,7 +29,11 @@ module Faraday
27
29
  end
28
30
 
29
31
  def parse(body)
30
- ::JSON.parse(body, @parser_options || {}) unless body.strip.empty?
32
+ return if body.strip.empty?
33
+
34
+ decoder, method_name = @decoder_options
35
+
36
+ decoder.public_send(method_name, body, @parser_options || {})
31
37
  end
32
38
 
33
39
  def parse_response?(env)
@@ -47,6 +53,19 @@ module Faraday
47
53
  type = type.split(';', 2).first if type.index(';')
48
54
  type
49
55
  end
56
+
57
+ def process_parser_options
58
+ @decoder_options = @parser_options&.delete(:decoder)
59
+
60
+ @decoder_options =
61
+ if @decoder_options.is_a?(Array) && @decoder_options.size >= 2
62
+ @decoder_options.slice(0, 2)
63
+ elsif @decoder_options.respond_to?(:load)
64
+ [@decoder_options, :load]
65
+ else
66
+ [::JSON, :parse]
67
+ end
68
+ end
50
69
  end
51
70
  end
52
71
  end
@@ -6,8 +6,8 @@ module Faraday
6
6
  # client or server error responses.
7
7
  class RaiseError < Middleware
8
8
  # rubocop:disable Naming/ConstantName
9
- ClientErrorStatuses = (400...500).freeze
10
- ServerErrorStatuses = (500...600).freeze
9
+ ClientErrorStatuses = (400...500)
10
+ ServerErrorStatuses = (500...600)
11
11
  # rubocop:enable Naming/ConstantName
12
12
 
13
13
  def on_complete(env)
@@ -24,10 +24,14 @@ module Faraday
24
24
  # mimic the behavior that we get with proxy requests with HTTPS
25
25
  msg = %(407 "Proxy Authentication Required")
26
26
  raise Faraday::ProxyAuthError.new(msg, response_values(env))
27
+ when 408
28
+ raise Faraday::RequestTimeoutError, response_values(env)
27
29
  when 409
28
30
  raise Faraday::ConflictError, response_values(env)
29
31
  when 422
30
32
  raise Faraday::UnprocessableEntityError, response_values(env)
33
+ when 429
34
+ raise Faraday::TooManyRequestsError, response_values(env)
31
35
  when ClientErrorStatuses
32
36
  raise Faraday::ClientError, response_values(env)
33
37
  when ServerErrorStatuses
@@ -37,11 +41,26 @@ module Faraday
37
41
  end
38
42
  end
39
43
 
44
+ # Returns a hash of response data with the following keys:
45
+ # - status
46
+ # - headers
47
+ # - body
48
+ # - request
49
+ #
50
+ # The `request` key is omitted when the middleware is explicitly
51
+ # configured with the option `include_request: false`.
40
52
  def response_values(env)
41
- {
53
+ response = {
42
54
  status: env.status,
43
55
  headers: env.response_headers,
44
- body: env.body,
56
+ body: env.body
57
+ }
58
+
59
+ # Include the request data by default. If the middleware was explicitly
60
+ # configured to _not_ include request data, then omit it.
61
+ return response unless options.fetch(:include_request, true)
62
+
63
+ response.merge(
45
64
  request: {
46
65
  method: env.method,
47
66
  url: env.url,
@@ -50,7 +69,7 @@ module Faraday
50
69
  headers: env.request_headers,
51
70
  body: env.request_body
52
71
  }
53
- }
72
+ )
54
73
  end
55
74
 
56
75
  def query_params(env)
data/lib/faraday/utils.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'base64'
4
3
  require 'uri'
5
4
  require 'faraday/utils/headers'
6
5
  require 'faraday/utils/params_hash'
@@ -26,7 +25,7 @@ module Faraday
26
25
  attr_writer :default_space_encoding
27
26
  end
28
27
 
29
- ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/.freeze
28
+ ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/
30
29
 
31
30
  def escape(str)
32
31
  str.to_s.gsub(ESCAPE_RE) do |match|
@@ -38,7 +37,7 @@ module Faraday
38
37
  CGI.unescape str.to_s
39
38
  end
40
39
 
41
- DEFAULT_SEP = /[&;] */n.freeze
40
+ DEFAULT_SEP = /[&;] */n
42
41
 
43
42
  # Adapted from Rack
44
43
  def parse_query(query)
@@ -54,7 +53,7 @@ module Faraday
54
53
  end
55
54
 
56
55
  def basic_header_from(login, pass)
57
- value = Base64.encode64("#{login}:#{pass}")
56
+ value = ["#{login}:#{pass}"].pack('m') # Base64 encoding
58
57
  value.delete!("\n")
59
58
  "Basic #{value}"
60
59
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Faraday
4
- VERSION = '2.7.4'
4
+ VERSION = '2.9.0'
5
5
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- RSpec.describe Faraday::ClientError do
3
+ RSpec.describe Faraday::Error do
4
4
  describe '.initialize' do
5
5
  subject { described_class.new(exception, response) }
6
6
  let(:response) { nil }
@@ -12,8 +12,10 @@ RSpec.describe Faraday::ClientError do
12
12
  it { expect(subject.response).to be_nil }
13
13
  it { expect(subject.message).to eq(exception.message) }
14
14
  it { expect(subject.backtrace).to eq(exception.backtrace) }
15
- it { expect(subject.inspect).to eq('#<Faraday::ClientError wrapped=#<RuntimeError: test>>') }
15
+ it { expect(subject.inspect).to eq('#<Faraday::Error wrapped=#<RuntimeError: test>>') }
16
16
  it { expect(subject.response_status).to be_nil }
17
+ it { expect(subject.response_headers).to be_nil }
18
+ it { expect(subject.response_body).to be_nil }
17
19
  end
18
20
 
19
21
  context 'with response hash' do
@@ -22,8 +24,10 @@ RSpec.describe Faraday::ClientError do
22
24
  it { expect(subject.wrapped_exception).to be_nil }
23
25
  it { expect(subject.response).to eq(exception) }
24
26
  it { expect(subject.message).to eq('the server responded with status 400') }
25
- it { expect(subject.inspect).to eq('#<Faraday::ClientError response={:status=>400}>') }
27
+ it { expect(subject.inspect).to eq('#<Faraday::Error response={:status=>400}>') }
26
28
  it { expect(subject.response_status).to eq(400) }
29
+ it { expect(subject.response_headers).to be_nil }
30
+ it { expect(subject.response_body).to be_nil }
27
31
  end
28
32
 
29
33
  context 'with string' do
@@ -32,8 +36,10 @@ RSpec.describe Faraday::ClientError do
32
36
  it { expect(subject.wrapped_exception).to be_nil }
33
37
  it { expect(subject.response).to be_nil }
34
38
  it { expect(subject.message).to eq('custom message') }
35
- it { expect(subject.inspect).to eq('#<Faraday::ClientError #<Faraday::ClientError: custom message>>') }
39
+ it { expect(subject.inspect).to eq('#<Faraday::Error #<Faraday::Error: custom message>>') }
36
40
  it { expect(subject.response_status).to be_nil }
41
+ it { expect(subject.response_headers).to be_nil }
42
+ it { expect(subject.response_body).to be_nil }
37
43
  end
38
44
 
39
45
  context 'with anything else #to_s' do
@@ -42,8 +48,10 @@ RSpec.describe Faraday::ClientError do
42
48
  it { expect(subject.wrapped_exception).to be_nil }
43
49
  it { expect(subject.response).to be_nil }
44
50
  it { expect(subject.message).to eq('["error1", "error2"]') }
45
- it { expect(subject.inspect).to eq('#<Faraday::ClientError #<Faraday::ClientError: ["error1", "error2"]>>') }
51
+ it { expect(subject.inspect).to eq('#<Faraday::Error #<Faraday::Error: ["error1", "error2"]>>') }
46
52
  it { expect(subject.response_status).to be_nil }
53
+ it { expect(subject.response_headers).to be_nil }
54
+ it { expect(subject.response_body).to be_nil }
47
55
  end
48
56
 
49
57
  context 'with exception string and response hash' do
@@ -53,8 +61,25 @@ RSpec.describe Faraday::ClientError do
53
61
  it { expect(subject.wrapped_exception).to be_nil }
54
62
  it { expect(subject.response).to eq(response) }
55
63
  it { expect(subject.message).to eq('custom message') }
56
- it { expect(subject.inspect).to eq('#<Faraday::ClientError response={:status=>400}>') }
64
+ it { expect(subject.inspect).to eq('#<Faraday::Error response={:status=>400}>') }
57
65
  it { expect(subject.response_status).to eq(400) }
66
+ it { expect(subject.response_headers).to be_nil }
67
+ it { expect(subject.response_body).to be_nil }
68
+ end
69
+
70
+ context 'with exception and response object' do
71
+ let(:exception) { RuntimeError.new('test') }
72
+ let(:body) { { test: 'test' } }
73
+ let(:headers) { { 'Content-Type' => 'application/json' } }
74
+ let(:response) { Faraday::Response.new(status: 400, response_headers: headers, response_body: body) }
75
+
76
+ it { expect(subject.wrapped_exception).to eq(exception) }
77
+ it { expect(subject.response).to eq(response) }
78
+ it { expect(subject.message).to eq(exception.message) }
79
+ it { expect(subject.backtrace).to eq(exception.backtrace) }
80
+ it { expect(subject.response_status).to eq(400) }
81
+ it { expect(subject.response_headers).to eq(headers) }
82
+ it { expect(subject.response_body).to eq(body) }
58
83
  end
59
84
  end
60
85
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  RSpec.describe Faraday::Options do
4
4
  SubOptions = Class.new(Faraday::Options.new(:sub_a, :sub_b))
5
- class ParentOptions < Faraday::Options.new(:a, :b, :c)
5
+ ParentOptions = Faraday::Options.new(:a, :b, :c) do
6
6
  options c: SubOptions
7
7
  end
8
8
 
@@ -32,6 +32,14 @@ RSpec.describe Faraday::ProxyOptions do
32
32
  expect(proxy.user).to be_nil
33
33
  expect(proxy.password).to be_nil
34
34
  end
35
+
36
+ it 'treats empty string as nil' do
37
+ proxy = nil
38
+ proxy_string = proxy.to_s # => empty string
39
+ options = Faraday::ProxyOptions.from proxy_string
40
+ expect(options).to be_a_kind_of(Faraday::ProxyOptions)
41
+ expect(options.inspect).to eq('#<Faraday::ProxyOptions (empty)>')
42
+ end
35
43
  end
36
44
 
37
45
  it 'allows hash access' do
@@ -73,6 +73,30 @@ RSpec.describe Faraday::Request::Json do
73
73
  end
74
74
  end
75
75
 
76
+ context 'true body' do
77
+ let(:result) { process(true) }
78
+
79
+ it 'encodes body' do
80
+ expect(result_body).to eq('true')
81
+ end
82
+
83
+ it 'adds content type' do
84
+ expect(result_type).to eq('application/json')
85
+ end
86
+ end
87
+
88
+ context 'false body' do
89
+ let(:result) { process(false) }
90
+
91
+ it 'encodes body' do
92
+ expect(result_body).to eq('false')
93
+ end
94
+
95
+ it 'adds content type' do
96
+ expect(result_type).to eq('application/json')
97
+ end
98
+ end
99
+
76
100
  context 'object body with json type' do
77
101
  let(:result) { process({ a: 1 }, 'application/json; charset=utf-8') }
78
102
 
@@ -108,4 +132,68 @@ RSpec.describe Faraday::Request::Json do
108
132
  expect(result_type).to eq('application/xml; charset=utf-8')
109
133
  end
110
134
  end
135
+
136
+ context 'with encoder' do
137
+ let(:encoder) do
138
+ double('Encoder').tap do |e|
139
+ allow(e).to receive(:dump) { |s, opts| JSON.generate(s, opts) }
140
+ end
141
+ end
142
+
143
+ let(:result) { process(a: 1) }
144
+
145
+ context 'when encoder is passed as object' do
146
+ let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }, { encoder: encoder }) }
147
+
148
+ it 'calls specified JSON encoder\'s dump method' do
149
+ expect(encoder).to receive(:dump).with({ a: 1 })
150
+
151
+ result
152
+ end
153
+
154
+ it 'encodes body' do
155
+ expect(result_body).to eq('{"a":1}')
156
+ end
157
+
158
+ it 'adds content type' do
159
+ expect(result_type).to eq('application/json')
160
+ end
161
+ end
162
+
163
+ context 'when encoder is passed as an object-method pair' do
164
+ let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }, { encoder: [encoder, :dump] }) }
165
+
166
+ it 'calls specified JSON encoder' do
167
+ expect(encoder).to receive(:dump).with({ a: 1 })
168
+
169
+ result
170
+ end
171
+
172
+ it 'encodes body' do
173
+ expect(result_body).to eq('{"a":1}')
174
+ end
175
+
176
+ it 'adds content type' do
177
+ expect(result_type).to eq('application/json')
178
+ end
179
+ end
180
+
181
+ context 'when encoder is not passed' do
182
+ let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }) }
183
+
184
+ it 'calls JSON.generate' do
185
+ expect(JSON).to receive(:generate).with({ a: 1 })
186
+
187
+ result
188
+ end
189
+
190
+ it 'encodes body' do
191
+ expect(result_body).to eq('{"a":1}')
192
+ end
193
+
194
+ it 'adds content type' do
195
+ expect(result_type).to eq('application/json')
196
+ end
197
+ end
198
+ end
111
199
  end
@@ -114,4 +114,76 @@ RSpec.describe Faraday::Response::Json, type: :response do
114
114
  expect(response.body).to eq(result)
115
115
  end
116
116
  end
117
+
118
+ context 'with decoder' do
119
+ let(:decoder) do
120
+ double('Decoder').tap do |e|
121
+ allow(e).to receive(:load) { |s, opts| JSON.parse(s, opts) }
122
+ end
123
+ end
124
+
125
+ let(:body) { '{"a": 1}' }
126
+ let(:result) { { a: 1 } }
127
+
128
+ context 'when decoder is passed as object' do
129
+ let(:options) do
130
+ {
131
+ parser_options: {
132
+ decoder: decoder,
133
+ option: :option_value,
134
+ symbolize_names: true
135
+ }
136
+ }
137
+ end
138
+
139
+ it 'passes relevant options to specified decoder\'s load method' do
140
+ expect(decoder).to receive(:load)
141
+ .with(body, { option: :option_value, symbolize_names: true })
142
+ .and_return(result)
143
+
144
+ response = process(body)
145
+ expect(response.body).to eq(result)
146
+ end
147
+ end
148
+
149
+ context 'when decoder is passed as an object-method pair' do
150
+ let(:options) do
151
+ {
152
+ parser_options: {
153
+ decoder: [decoder, :load],
154
+ option: :option_value,
155
+ symbolize_names: true
156
+ }
157
+ }
158
+ end
159
+
160
+ it 'passes relevant options to specified decoder\'s method' do
161
+ expect(decoder).to receive(:load)
162
+ .with(body, { option: :option_value, symbolize_names: true })
163
+ .and_return(result)
164
+
165
+ response = process(body)
166
+ expect(response.body).to eq(result)
167
+ end
168
+ end
169
+
170
+ context 'when decoder is not passed' do
171
+ let(:options) do
172
+ {
173
+ parser_options: {
174
+ symbolize_names: true
175
+ }
176
+ }
177
+ end
178
+
179
+ it 'passes relevant options to JSON parse' do
180
+ expect(JSON).to receive(:parse)
181
+ .with(body, { symbolize_names: true })
182
+ .and_return(result)
183
+
184
+ response = process(body)
185
+ expect(response.body).to eq(result)
186
+ end
187
+ end
188
+ end
117
189
  end
@@ -25,6 +25,7 @@ RSpec.describe Faraday::Response::Logger do
25
25
  stubs.get('/filtered_headers') { [200, { 'Content-Type' => 'text/html' }, 'headers response'] }
26
26
  stubs.get('/filtered_params') { [200, { 'Content-Type' => 'text/html' }, 'params response'] }
27
27
  stubs.get('/filtered_url') { [200, { 'Content-Type' => 'text/html' }, 'url response'] }
28
+ stubs.get('/connection_failed') { raise Faraday::ConnectionFailed, 'Failed to open TCP connection' }
28
29
  end
29
30
  end
30
31
  end
@@ -216,6 +217,15 @@ RSpec.describe Faraday::Response::Logger do
216
217
  end
217
218
  end
218
219
 
220
+ context 'when logging headers and errors' do
221
+ let(:logger_options) { { headers: true, errors: true } }
222
+
223
+ it 'logs error message' do
224
+ expect { conn.get '/connection_failed' }.to raise_error(Faraday::ConnectionFailed)
225
+ expect(string_io.string).to match(%(Failed to open TCP connection))
226
+ end
227
+ end
228
+
219
229
  context 'when using log_level' do
220
230
  let(:logger_options) { { bodies: true, log_level: :debug } }
221
231