faraday 1.0.0 → 2.10.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -1
  3. data/LICENSE.md +1 -1
  4. data/README.md +35 -23
  5. data/Rakefile +6 -1
  6. data/examples/client_spec.rb +68 -14
  7. data/examples/client_test.rb +80 -15
  8. data/lib/faraday/adapter/test.rb +117 -52
  9. data/lib/faraday/adapter.rb +7 -21
  10. data/lib/faraday/adapter_registry.rb +3 -1
  11. data/lib/faraday/connection.rb +75 -133
  12. data/lib/faraday/encoders/flat_params_encoder.rb +9 -2
  13. data/lib/faraday/encoders/nested_params_encoder.rb +20 -8
  14. data/lib/faraday/error.rb +39 -6
  15. data/lib/faraday/logging/formatter.rb +28 -15
  16. data/lib/faraday/methods.rb +6 -0
  17. data/lib/faraday/middleware.rb +59 -5
  18. data/lib/faraday/middleware_registry.rb +17 -63
  19. data/lib/faraday/options/connection_options.rb +7 -6
  20. data/lib/faraday/options/env.rb +85 -62
  21. data/lib/faraday/options/proxy_options.rb +11 -3
  22. data/lib/faraday/options/request_options.rb +7 -6
  23. data/lib/faraday/options/ssl_options.rb +56 -45
  24. data/lib/faraday/options.rb +11 -14
  25. data/lib/faraday/rack_builder.rb +35 -32
  26. data/lib/faraday/request/authorization.rb +37 -36
  27. data/lib/faraday/request/instrumentation.rb +5 -1
  28. data/lib/faraday/request/json.rb +70 -0
  29. data/lib/faraday/request/url_encoded.rb +8 -2
  30. data/lib/faraday/request.rb +22 -29
  31. data/lib/faraday/response/json.rb +74 -0
  32. data/lib/faraday/response/logger.rb +8 -4
  33. data/lib/faraday/response/raise_error.rb +43 -3
  34. data/lib/faraday/response.rb +10 -23
  35. data/lib/faraday/utils/headers.rb +17 -6
  36. data/lib/faraday/utils.rb +22 -10
  37. data/lib/faraday/version.rb +5 -0
  38. data/lib/faraday.rb +49 -58
  39. data/spec/faraday/adapter/test_spec.rb +442 -0
  40. data/spec/faraday/connection_spec.rb +207 -90
  41. data/spec/faraday/error_spec.rb +45 -5
  42. data/spec/faraday/middleware_registry_spec.rb +31 -0
  43. data/spec/faraday/middleware_spec.rb +193 -6
  44. data/spec/faraday/options/env_spec.rb +8 -2
  45. data/spec/faraday/options/options_spec.rb +1 -1
  46. data/spec/faraday/options/proxy_options_spec.rb +15 -0
  47. data/spec/faraday/params_encoders/flat_spec.rb +8 -0
  48. data/spec/faraday/params_encoders/nested_spec.rb +18 -1
  49. data/spec/faraday/rack_builder_spec.rb +171 -50
  50. data/spec/faraday/request/authorization_spec.rb +54 -24
  51. data/spec/faraday/request/instrumentation_spec.rb +5 -7
  52. data/spec/faraday/request/json_spec.rb +199 -0
  53. data/spec/faraday/request/url_encoded_spec.rb +25 -2
  54. data/spec/faraday/request_spec.rb +11 -10
  55. data/spec/faraday/response/json_spec.rb +206 -0
  56. data/spec/faraday/response/logger_spec.rb +38 -0
  57. data/spec/faraday/response/raise_error_spec.rb +149 -0
  58. data/spec/faraday/response_spec.rb +3 -1
  59. data/spec/faraday/utils/headers_spec.rb +31 -4
  60. data/spec/faraday/utils_spec.rb +63 -1
  61. data/spec/faraday_spec.rb +10 -4
  62. data/spec/spec_helper.rb +6 -5
  63. data/spec/support/fake_safe_buffer.rb +1 -1
  64. data/spec/support/faraday_middleware_subclasses.rb +18 -0
  65. data/spec/support/helper_methods.rb +0 -37
  66. data/spec/support/shared_examples/adapter.rb +4 -3
  67. data/spec/support/shared_examples/request_method.rb +60 -31
  68. metadata +34 -44
  69. data/UPGRADING.md +0 -55
  70. data/lib/faraday/adapter/em_http.rb +0 -285
  71. data/lib/faraday/adapter/em_http_ssl_patch.rb +0 -62
  72. data/lib/faraday/adapter/em_synchrony/parallel_manager.rb +0 -69
  73. data/lib/faraday/adapter/em_synchrony.rb +0 -150
  74. data/lib/faraday/adapter/excon.rb +0 -124
  75. data/lib/faraday/adapter/httpclient.rb +0 -151
  76. data/lib/faraday/adapter/net_http.rb +0 -209
  77. data/lib/faraday/adapter/net_http_persistent.rb +0 -91
  78. data/lib/faraday/adapter/patron.rb +0 -132
  79. data/lib/faraday/adapter/rack.rb +0 -75
  80. data/lib/faraday/adapter/typhoeus.rb +0 -15
  81. data/lib/faraday/autoload.rb +0 -95
  82. data/lib/faraday/dependency_loader.rb +0 -37
  83. data/lib/faraday/file_part.rb +0 -128
  84. data/lib/faraday/param_part.rb +0 -53
  85. data/lib/faraday/request/basic_authentication.rb +0 -20
  86. data/lib/faraday/request/multipart.rb +0 -99
  87. data/lib/faraday/request/retry.rb +0 -239
  88. data/lib/faraday/request/token_authentication.rb +0 -20
  89. data/spec/faraday/adapter/em_http_spec.rb +0 -47
  90. data/spec/faraday/adapter/em_synchrony_spec.rb +0 -16
  91. data/spec/faraday/adapter/excon_spec.rb +0 -49
  92. data/spec/faraday/adapter/httpclient_spec.rb +0 -73
  93. data/spec/faraday/adapter/net_http_persistent_spec.rb +0 -57
  94. data/spec/faraday/adapter/net_http_spec.rb +0 -64
  95. data/spec/faraday/adapter/patron_spec.rb +0 -18
  96. data/spec/faraday/adapter/rack_spec.rb +0 -8
  97. data/spec/faraday/adapter/typhoeus_spec.rb +0 -7
  98. data/spec/faraday/composite_read_io_spec.rb +0 -80
  99. data/spec/faraday/request/multipart_spec.rb +0 -274
  100. data/spec/faraday/request/retry_spec.rb +0 -242
  101. data/spec/faraday/response/middleware_spec.rb +0 -52
  102. data/spec/support/webmock_rack_app.rb +0 -68
@@ -2,24 +2,25 @@
2
2
 
3
3
  RSpec.describe Faraday::Request do
4
4
  let(:conn) do
5
- Faraday.new(url: 'http://sushi.com/api',
5
+ Faraday.new(url: 'http://httpbingo.org/api',
6
6
  headers: { 'Mime-Version' => '1.0' },
7
7
  request: { oauth: { consumer_key: 'anonymous' } })
8
8
  end
9
- let(:method) { :get }
9
+ let(:http_method) { :get }
10
10
  let(:block) { nil }
11
11
 
12
- subject { conn.build_request(method, &block) }
12
+ subject { conn.build_request(http_method, &block) }
13
13
 
14
14
  context 'when nothing particular is configured' do
15
- it { expect(subject.method).to eq(:get) }
15
+ it { expect(subject.http_method).to eq(:get) }
16
16
  it { expect(subject.to_env(conn).ssl.verify).to be_falsey }
17
+ it { expect(subject.to_env(conn).ssl.verify_hostname).to be_falsey }
17
18
  end
18
19
 
19
- context 'when method is post' do
20
- let(:method) { :post }
20
+ context 'when HTTP method is post' do
21
+ let(:http_method) { :post }
21
22
 
22
- it { expect(subject.method).to eq(:post) }
23
+ it { expect(subject.http_method).to eq(:post) }
23
24
  end
24
25
 
25
26
  context 'when setting the url on setup with a URI' do
@@ -27,7 +28,7 @@ RSpec.describe Faraday::Request do
27
28
 
28
29
  it { expect(subject.path).to eq(URI.parse('foo.json')) }
29
30
  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
+ it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1') }
31
32
  end
32
33
 
33
34
  context 'when setting the url on setup with a string path and params' do
@@ -35,7 +36,7 @@ RSpec.describe Faraday::Request do
35
36
 
36
37
  it { expect(subject.path).to eq('foo.json') }
37
38
  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
+ it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1') }
39
40
  end
40
41
 
41
42
  context 'when setting the url on setup with a path including params' do
@@ -43,7 +44,7 @@ RSpec.describe Faraday::Request do
43
44
 
44
45
  it { expect(subject.path).to eq('foo.json') }
45
46
  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
+ it { expect(subject.to_env(conn).url.to_s).to eq('http://httpbingo.org/api/foo.json?a=1&b=2') }
47
48
  end
48
49
 
49
50
  context 'when setting a header on setup with []= syntax' do
@@ -0,0 +1,206 @@
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
+
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
+
188
+ it 'passes relevant options to JSON parse even when nil responds to :load' do
189
+ original_allow_message_expectations_on_nil = RSpec::Mocks.configuration.allow_message_expectations_on_nil
190
+ RSpec::Mocks.configuration.allow_message_expectations_on_nil = true
191
+ allow(nil).to receive(:respond_to?)
192
+ .with(:load)
193
+ .and_return(true)
194
+
195
+ expect(JSON).to receive(:parse)
196
+ .with(body, { symbolize_names: true })
197
+ .and_return(result)
198
+
199
+ response = process(body)
200
+ expect(response.body).to eq(result)
201
+ ensure
202
+ RSpec::Mocks.configuration.allow_message_expectations_on_nil = original_allow_message_expectations_on_nil
203
+ end
204
+ end
205
+ end
206
+ 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
@@ -64,6 +65,15 @@ RSpec.describe Faraday::Response::Logger do
64
65
  expect(formatter).to receive(:response).with(an_instance_of(Faraday::Env))
65
66
  conn.get '/hello'
66
67
  end
68
+
69
+ context 'when no route' do
70
+ it 'delegates logging to the formatter' do
71
+ expect(formatter).to receive(:request).with(an_instance_of(Faraday::Env))
72
+ expect(formatter).to receive(:exception).with(an_instance_of(Faraday::Adapter::Test::Stubs::NotFound))
73
+
74
+ expect { conn.get '/noroute' }.to raise_error(Faraday::Adapter::Test::Stubs::NotFound)
75
+ end
76
+ end
67
77
  end
68
78
 
69
79
  context 'with custom formatter' do
@@ -94,6 +104,16 @@ RSpec.describe Faraday::Response::Logger do
94
104
  expect(string_io.string).to match('GET http:/hello')
95
105
  end
96
106
 
107
+ it 'logs status' do
108
+ conn.get '/hello', nil, accept: 'text/html'
109
+ expect(string_io.string).to match('Status 200')
110
+ end
111
+
112
+ it 'does not log error message by default' do
113
+ expect { conn.get '/noroute' }.to raise_error(Faraday::Adapter::Test::Stubs::NotFound)
114
+ expect(string_io.string).not_to match(%(no stubbed request for get http:/noroute))
115
+ end
116
+
97
117
  it 'logs request headers by default' do
98
118
  conn.get '/hello', nil, accept: 'text/html'
99
119
  expect(string_io.string).to match(%(Accept: "text/html))
@@ -188,6 +208,24 @@ RSpec.describe Faraday::Response::Logger do
188
208
  end
189
209
  end
190
210
 
211
+ context 'when logging errors' do
212
+ let(:logger_options) { { errors: true } }
213
+
214
+ it 'logs error message' do
215
+ expect { conn.get '/noroute' }.to raise_error(Faraday::Adapter::Test::Stubs::NotFound)
216
+ expect(string_io.string).to match(%(no stubbed request for get http:/noroute))
217
+ end
218
+ end
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
+
191
229
  context 'when using log_level' do
192
230
  let(:logger_options) { { bodies: true, log_level: :debug } }
193
231
 
@@ -11,8 +11,10 @@ RSpec.describe Faraday::Response::RaiseError do
11
11
  stub.get('forbidden') { [403, { 'X-Reason' => 'because' }, 'keep looking'] }
12
12
  stub.get('not-found') { [404, { 'X-Reason' => 'because' }, 'keep looking'] }
13
13
  stub.get('proxy-error') { [407, { 'X-Reason' => 'because' }, 'keep looking'] }
14
+ stub.get('request-timeout') { [408, { 'X-Reason' => 'because' }, 'keep looking'] }
14
15
  stub.get('conflict') { [409, { 'X-Reason' => 'because' }, 'keep looking'] }
15
16
  stub.get('unprocessable-entity') { [422, { 'X-Reason' => 'because' }, 'keep looking'] }
17
+ stub.get('too-many-requests') { [429, { 'X-Reason' => 'because' }, 'keep looking'] }
16
18
  stub.get('4xx') { [499, { 'X-Reason' => 'because' }, 'keep looking'] }
17
19
  stub.get('nil-status') { [nil, { 'X-Reason' => 'nil' }, 'fail'] }
18
20
  stub.get('server-error') { [500, { 'X-Error' => 'bailout' }, 'fail'] }
@@ -29,6 +31,9 @@ RSpec.describe Faraday::Response::RaiseError do
29
31
  expect(ex.message).to eq('the server responded with status 400')
30
32
  expect(ex.response[:headers]['X-Reason']).to eq('because')
31
33
  expect(ex.response[:status]).to eq(400)
34
+ expect(ex.response_status).to eq(400)
35
+ expect(ex.response_body).to eq('keep looking')
36
+ expect(ex.response_headers['X-Reason']).to eq('because')
32
37
  end
33
38
  end
34
39
 
@@ -37,6 +42,9 @@ RSpec.describe Faraday::Response::RaiseError do
37
42
  expect(ex.message).to eq('the server responded with status 401')
38
43
  expect(ex.response[:headers]['X-Reason']).to eq('because')
39
44
  expect(ex.response[:status]).to eq(401)
45
+ expect(ex.response_status).to eq(401)
46
+ expect(ex.response_body).to eq('keep looking')
47
+ expect(ex.response_headers['X-Reason']).to eq('because')
40
48
  end
41
49
  end
42
50
 
@@ -45,6 +53,9 @@ RSpec.describe Faraday::Response::RaiseError do
45
53
  expect(ex.message).to eq('the server responded with status 403')
46
54
  expect(ex.response[:headers]['X-Reason']).to eq('because')
47
55
  expect(ex.response[:status]).to eq(403)
56
+ expect(ex.response_status).to eq(403)
57
+ expect(ex.response_body).to eq('keep looking')
58
+ expect(ex.response_headers['X-Reason']).to eq('because')
48
59
  end
49
60
  end
50
61
 
@@ -53,6 +64,9 @@ RSpec.describe Faraday::Response::RaiseError do
53
64
  expect(ex.message).to eq('the server responded with status 404')
54
65
  expect(ex.response[:headers]['X-Reason']).to eq('because')
55
66
  expect(ex.response[:status]).to eq(404)
67
+ expect(ex.response_status).to eq(404)
68
+ expect(ex.response_body).to eq('keep looking')
69
+ expect(ex.response_headers['X-Reason']).to eq('because')
56
70
  end
57
71
  end
58
72
 
@@ -61,6 +75,20 @@ RSpec.describe Faraday::Response::RaiseError do
61
75
  expect(ex.message).to eq('407 "Proxy Authentication Required"')
62
76
  expect(ex.response[:headers]['X-Reason']).to eq('because')
63
77
  expect(ex.response[:status]).to eq(407)
78
+ expect(ex.response_status).to eq(407)
79
+ expect(ex.response_body).to eq('keep looking')
80
+ expect(ex.response_headers['X-Reason']).to eq('because')
81
+ end
82
+ end
83
+
84
+ it 'raises Faraday::RequestTimeoutError for 408 responses' do
85
+ expect { conn.get('request-timeout') }.to raise_error(Faraday::RequestTimeoutError) do |ex|
86
+ expect(ex.message).to eq('the server responded with status 408')
87
+ expect(ex.response[:headers]['X-Reason']).to eq('because')
88
+ expect(ex.response[:status]).to eq(408)
89
+ expect(ex.response_status).to eq(408)
90
+ expect(ex.response_body).to eq('keep looking')
91
+ expect(ex.response_headers['X-Reason']).to eq('because')
64
92
  end
65
93
  end
66
94
 
@@ -69,6 +97,9 @@ RSpec.describe Faraday::Response::RaiseError do
69
97
  expect(ex.message).to eq('the server responded with status 409')
70
98
  expect(ex.response[:headers]['X-Reason']).to eq('because')
71
99
  expect(ex.response[:status]).to eq(409)
100
+ expect(ex.response_status).to eq(409)
101
+ expect(ex.response_body).to eq('keep looking')
102
+ expect(ex.response_headers['X-Reason']).to eq('because')
72
103
  end
73
104
  end
74
105
 
@@ -77,6 +108,20 @@ RSpec.describe Faraday::Response::RaiseError do
77
108
  expect(ex.message).to eq('the server responded with status 422')
78
109
  expect(ex.response[:headers]['X-Reason']).to eq('because')
79
110
  expect(ex.response[:status]).to eq(422)
111
+ expect(ex.response_status).to eq(422)
112
+ expect(ex.response_body).to eq('keep looking')
113
+ expect(ex.response_headers['X-Reason']).to eq('because')
114
+ end
115
+ end
116
+
117
+ it 'raises Faraday::TooManyRequestsError for 429 responses' do
118
+ expect { conn.get('too-many-requests') }.to raise_error(Faraday::TooManyRequestsError) do |ex|
119
+ expect(ex.message).to eq('the server responded with status 429')
120
+ expect(ex.response[:headers]['X-Reason']).to eq('because')
121
+ expect(ex.response[:status]).to eq(429)
122
+ expect(ex.response_status).to eq(429)
123
+ expect(ex.response_body).to eq('keep looking')
124
+ expect(ex.response_headers['X-Reason']).to eq('because')
80
125
  end
81
126
  end
82
127
 
@@ -85,6 +130,9 @@ RSpec.describe Faraday::Response::RaiseError do
85
130
  expect(ex.message).to eq('http status could not be derived from the server response')
86
131
  expect(ex.response[:headers]['X-Reason']).to eq('nil')
87
132
  expect(ex.response[:status]).to be_nil
133
+ expect(ex.response_status).to be_nil
134
+ expect(ex.response_body).to eq('fail')
135
+ expect(ex.response_headers['X-Reason']).to eq('nil')
88
136
  end
89
137
  end
90
138
 
@@ -93,6 +141,9 @@ RSpec.describe Faraday::Response::RaiseError do
93
141
  expect(ex.message).to eq('the server responded with status 499')
94
142
  expect(ex.response[:headers]['X-Reason']).to eq('because')
95
143
  expect(ex.response[:status]).to eq(499)
144
+ expect(ex.response_status).to eq(499)
145
+ expect(ex.response_body).to eq('keep looking')
146
+ expect(ex.response_headers['X-Reason']).to eq('because')
96
147
  end
97
148
  end
98
149
 
@@ -101,6 +152,104 @@ RSpec.describe Faraday::Response::RaiseError do
101
152
  expect(ex.message).to eq('the server responded with status 500')
102
153
  expect(ex.response[:headers]['X-Error']).to eq('bailout')
103
154
  expect(ex.response[:status]).to eq(500)
155
+ expect(ex.response_status).to eq(500)
156
+ expect(ex.response_body).to eq('fail')
157
+ expect(ex.response_headers['X-Error']).to eq('bailout')
158
+ end
159
+ end
160
+
161
+ describe 'request info' do
162
+ let(:conn) do
163
+ Faraday.new do |b|
164
+ b.response :raise_error, **middleware_options
165
+ b.adapter :test do |stub|
166
+ stub.post(url, request_body, request_headers) do
167
+ [400, { 'X-Reason' => 'because' }, 'keep looking']
168
+ end
169
+ end
170
+ end
171
+ end
172
+ let(:middleware_options) { {} }
173
+ let(:request_body) { JSON.generate({ 'item' => 'sth' }) }
174
+ let(:request_headers) { { 'Authorization' => 'Basic 123' } }
175
+ let(:url_path) { 'request' }
176
+ let(:query_params) { 'full=true' }
177
+ let(:url) { "#{url_path}?#{query_params}" }
178
+
179
+ subject(:perform_request) do
180
+ conn.post url do |req|
181
+ req.headers['Authorization'] = 'Basic 123'
182
+ req.body = request_body
183
+ end
184
+ end
185
+
186
+ it 'returns the request info in the exception' do
187
+ expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
188
+ expect(ex.response[:request][:method]).to eq(:post)
189
+ expect(ex.response[:request][:url]).to eq(URI("http:/#{url}"))
190
+ expect(ex.response[:request][:url_path]).to eq("/#{url_path}")
191
+ expect(ex.response[:request][:params]).to eq({ 'full' => 'true' })
192
+ expect(ex.response[:request][:headers]).to match(a_hash_including(request_headers))
193
+ expect(ex.response[:request][:body]).to eq(request_body)
194
+ end
195
+ end
196
+
197
+ describe 'DEFAULT_OPTION: include_request' do
198
+ before(:each) do
199
+ Faraday::Response::RaiseError.instance_variable_set(:@default_options, nil)
200
+ Faraday::Middleware.instance_variable_set(:@default_options, nil)
201
+ end
202
+
203
+ after(:all) do
204
+ Faraday::Response::RaiseError.instance_variable_set(:@default_options, nil)
205
+ Faraday::Middleware.instance_variable_set(:@default_options, nil)
206
+ end
207
+
208
+ context 'when RaiseError DEFAULT_OPTION (include_request: true) is used' do
209
+ it 'includes request info in the exception' do
210
+ expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
211
+ expect(ex.response.keys).to contain_exactly(
212
+ :status,
213
+ :headers,
214
+ :body,
215
+ :request
216
+ )
217
+ end
218
+ end
219
+ end
220
+
221
+ context 'when application sets default_options `include_request: false`' do
222
+ before(:each) do
223
+ Faraday::Response::RaiseError.default_options = { include_request: false }
224
+ end
225
+
226
+ context 'and when include_request option is omitted' do
227
+ it 'does not include request info in the exception' do
228
+ expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
229
+ expect(ex.response.keys).to contain_exactly(
230
+ :status,
231
+ :headers,
232
+ :body
233
+ )
234
+ end
235
+ end
236
+ end
237
+
238
+ context 'and when include_request option is explicitly set for instance' do
239
+ let(:middleware_options) { { include_request: true } }
240
+
241
+ it 'includes request info in the exception' do
242
+ expect { perform_request }.to raise_error(Faraday::BadRequestError) do |ex|
243
+ expect(ex.response.keys).to contain_exactly(
244
+ :status,
245
+ :headers,
246
+ :body,
247
+ :request
248
+ )
249
+ end
250
+ end
251
+ end
252
+ end
104
253
  end
105
254
  end
106
255
  end
@@ -4,7 +4,7 @@ RSpec.describe Faraday::Response do
4
4
  subject { Faraday::Response.new(env) }
5
5
 
6
6
  let(:env) do
7
- Faraday::Env.from(status: 404, body: 'yikes',
7
+ Faraday::Env.from(status: 404, body: 'yikes', url: Faraday::Utils.URI('https://lostisland.github.io/faraday'),
8
8
  response_headers: { 'Content-Type' => 'text/plain' })
9
9
  end
10
10
 
@@ -30,6 +30,7 @@ RSpec.describe Faraday::Response do
30
30
  it { expect(hash[:status]).to eq(subject.status) }
31
31
  it { expect(hash[:response_headers]).to eq(subject.headers) }
32
32
  it { expect(hash[:body]).to eq(subject.body) }
33
+ it { expect(hash[:url]).to eq(subject.env.url) }
33
34
  end
34
35
 
35
36
  describe 'marshal serialization support' do
@@ -45,6 +46,7 @@ RSpec.describe Faraday::Response do
45
46
  it { expect(loaded.env[:body]).to eq(env[:body]) }
46
47
  it { expect(loaded.env[:response_headers]).to eq(env[:response_headers]) }
47
48
  it { expect(loaded.env[:status]).to eq(env[:status]) }
49
+ it { expect(loaded.env[:url]).to eq(env[:url]) }
48
50
  end
49
51
 
50
52
  describe '#on_complete' do
@@ -56,27 +56,54 @@ RSpec.describe Faraday::Utils::Headers do
56
56
  it { expect(subject.delete('content-type')).to be_nil }
57
57
  end
58
58
 
59
- describe '#parse' do
60
- before { subject.parse(headers) }
59
+ describe '#dig' do
60
+ before { subject['Content-Type'] = 'application/json' }
61
+
62
+ it { expect(subject&.dig('Content-Type')).to eq('application/json') }
63
+ it { expect(subject&.dig('CONTENT-TYPE')).to eq('application/json') }
64
+ it { expect(subject&.dig(:content_type)).to eq('application/json') }
65
+ it { expect(subject&.dig('invalid')).to be_nil }
66
+ end
61
67
 
68
+ describe '#parse' do
62
69
  context 'when response headers leave http status line out' do
63
70
  let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" }
64
71
 
72
+ before { subject.parse(headers) }
73
+
65
74
  it { expect(subject.keys).to eq(%w[Content-Type]) }
66
75
  it { expect(subject['Content-Type']).to eq('text/html') }
67
76
  it { expect(subject['content-type']).to eq('text/html') }
68
77
  end
69
78
 
70
79
  context 'when response headers values include a colon' do
71
- let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nLocation: http://sushi.com/\r\n\r\n" }
80
+ let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nLocation: http://httpbingo.org/\r\n\r\n" }
72
81
 
73
- it { expect(subject['location']).to eq('http://sushi.com/') }
82
+ before { subject.parse(headers) }
83
+
84
+ it { expect(subject['location']).to eq('http://httpbingo.org/') }
74
85
  end
75
86
 
76
87
  context 'when response headers include a blank line' do
77
88
  let(:headers) { "HTTP/1.1 200 OK\r\n\r\nContent-Type: text/html\r\n\r\n" }
78
89
 
90
+ before { subject.parse(headers) }
91
+
79
92
  it { expect(subject['content-type']).to eq('text/html') }
80
93
  end
94
+
95
+ context 'when response headers include already stored keys' do
96
+ let(:headers) { "HTTP/1.1 200 OK\r\nX-Numbers: 123\r\n\r\n" }
97
+
98
+ before do
99
+ h = subject
100
+ h[:x_numbers] = 8
101
+ h.parse(headers)
102
+ end
103
+
104
+ it do
105
+ expect(subject[:x_numbers]).to eq('8, 123')
106
+ end
107
+ end
81
108
  end
82
109
  end