oauth2 1.4.9 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,33 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OAuth2
4
- class AccessToken
5
- attr_reader :client, :token, :expires_in, :expires_at, :params
6
- attr_accessor :options, :refresh_token
4
+ class AccessToken # rubocop:disable Metrics/ClassLength
5
+ attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
6
+ attr_accessor :options, :refresh_token, :response
7
7
 
8
- # Should these methods be deprecated?
9
8
  class << self
10
9
  # Initializes an AccessToken from a Hash
11
10
  #
12
- # @param [Client] the OAuth2::Client instance
13
- # @param [Hash] a hash of AccessToken property values
14
- # @return [AccessToken] the initalized AccessToken
11
+ # @param [Client] client the OAuth2::Client instance
12
+ # @param [Hash] hash a hash of AccessToken property values
13
+ # @option hash [String] 'access_token', 'id_token', 'token', :access_token, :id_token, or :token the access token
14
+ # @return [AccessToken] the initialized AccessToken
15
15
  def from_hash(client, hash)
16
16
  hash = hash.dup
17
- new(client, hash.delete('access_token') || hash.delete(:access_token), hash)
17
+ token = hash.delete('access_token') || hash.delete(:access_token) ||
18
+ hash.delete('id_token') || hash.delete(:id_token) ||
19
+ hash.delete('token') || hash.delete(:token) ||
20
+ hash.delete('accessToken') || hash.delete(:accessToken) ||
21
+ hash.delete('idToken') || hash.delete(:idToken)
22
+ new(client, token, hash)
18
23
  end
19
24
 
20
25
  # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
21
26
  #
22
27
  # @param [Client] client the OAuth2::Client instance
23
28
  # @param [String] kvform the application/x-www-form-urlencoded string
24
- # @return [AccessToken] the initalized AccessToken
29
+ # @return [AccessToken] the initialized AccessToken
25
30
  def from_kvform(client, kvform)
26
31
  from_hash(client, Rack::Utils.parse_query(kvform))
27
32
  end
28
33
  end
29
34
 
30
- # Initalize an AccessToken
35
+ # Initialize an AccessToken
31
36
  #
32
37
  # @param [Client] client the OAuth2::Client instance
33
38
  # @param [String] token the Access Token value
@@ -35,6 +40,7 @@ module OAuth2
35
40
  # @option opts [String] :refresh_token (nil) the refresh_token value
36
41
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
37
42
  # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
43
+ # @option opts [FixNum, String] :expires_latency (nil) the number of seconds by which AccessToken validity will be reduced to offset latency, @version 2.0+
38
44
  # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
39
45
  # one of :header, :body or :query
40
46
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
@@ -43,17 +49,24 @@ module OAuth2
43
49
  def initialize(client, token, opts = {})
44
50
  @client = client
45
51
  @token = token.to_s
52
+
53
+ if @client.options[:raise_errors] && (@token.nil? || @token.empty?)
54
+ error = Error.new(opts)
55
+ raise(error)
56
+ end
46
57
  opts = opts.dup
47
- [:refresh_token, :expires_in, :expires_at].each do |arg|
58
+ %i[refresh_token expires_in expires_at expires_latency].each do |arg|
48
59
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
49
60
  end
50
61
  @expires_in ||= opts.delete('expires')
51
62
  @expires_in &&= @expires_in.to_i
52
63
  @expires_at &&= convert_expires_at(@expires_at)
64
+ @expires_latency &&= @expires_latency.to_i
53
65
  @expires_at ||= Time.now.to_i + @expires_in if @expires_in
54
- @options = {:mode => opts.delete(:mode) || :header,
55
- :header_format => opts.delete(:header_format) || 'Bearer %s',
56
- :param_name => opts.delete(:param_name) || 'access_token'}
66
+ @expires_at -= @expires_latency if @expires_latency
67
+ @options = {mode: opts.delete(:mode) || :header,
68
+ header_format: opts.delete(:header_format) || 'Bearer %s',
69
+ param_name: opts.delete(:param_name) || 'access_token'}
57
70
  @params = opts
58
71
  end
59
72
 
@@ -75,29 +88,36 @@ module OAuth2
75
88
  #
76
89
  # @return [Boolean]
77
90
  def expired?
78
- expires? && (expires_at < Time.now.to_i)
91
+ expires? && (expires_at <= Time.now.to_i)
79
92
  end
80
93
 
81
94
  # Refreshes the current Access Token
82
95
  #
83
96
  # @return [AccessToken] a new AccessToken
84
97
  # @note options should be carried over to the new AccessToken
85
- def refresh!(params = {})
98
+ def refresh(params = {}, access_token_opts = {})
86
99
  raise('A refresh_token is not available') unless refresh_token
87
100
 
88
101
  params[:grant_type] = 'refresh_token'
89
102
  params[:refresh_token] = refresh_token
90
- new_token = @client.get_token(params)
103
+ new_token = @client.get_token(params, access_token_opts)
91
104
  new_token.options = options
92
- new_token.refresh_token = refresh_token unless new_token.refresh_token
105
+ if new_token.refresh_token
106
+ # Keep it, if there is one
107
+ else
108
+ new_token.refresh_token = refresh_token
109
+ end
93
110
  new_token
94
111
  end
112
+ # A compatibility alias
113
+ # @note does not modify the receiver, so bang is not the default method
114
+ alias refresh! refresh
95
115
 
96
116
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
97
117
  #
98
118
  # @return [Hash] a hash of AccessToken property values
99
119
  def to_hash
100
- params.merge(:access_token => token, :refresh_token => refresh_token, :expires_at => expires_at)
120
+ params.merge(access_token: token, refresh_token: refresh_token, expires_at: expires_at)
101
121
  end
102
122
 
103
123
  # Make a request with the Access Token
@@ -105,7 +125,7 @@ module OAuth2
105
125
  # @param [Symbol] verb the HTTP request method
106
126
  # @param [String] path the HTTP URL path of the request
107
127
  # @param [Hash] opts the options to make the request with
108
- # @see Client#request
128
+ # @see Client#request
109
129
  def request(verb, path, opts = {}, &block)
110
130
  configure_authentication!(opts)
111
131
  @client.request(verb, path, opts, &block)
@@ -166,7 +186,7 @@ module OAuth2
166
186
  if opts[:body].is_a?(Hash)
167
187
  opts[:body][options[:param_name]] = token
168
188
  else
169
- opts[:body] << "&#{options[:param_name]}=#{token}"
189
+ opts[:body] += "&#{options[:param_name]}=#{token}"
170
190
  end
171
191
  # @todo support for multi-part (file uploads)
172
192
  else
@@ -37,7 +37,7 @@ module OAuth2
37
37
  end
38
38
 
39
39
  def self.encode_basic_auth(user, password)
40
- 'Basic ' + Base64.encode64(user + ':' + password).delete("\n")
40
+ "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
41
41
  end
42
42
 
43
43
  private
@@ -45,13 +45,18 @@ module OAuth2
45
45
  # Adds client_id and client_secret request parameters if they are not
46
46
  # already set.
47
47
  def apply_params_auth(params)
48
- {'client_id' => id, 'client_secret' => secret}.merge(params)
48
+ result = {}
49
+ result['client_id'] = id unless id.nil?
50
+ result['client_secret'] = secret unless secret.nil?
51
+ result.merge(params)
49
52
  end
50
53
 
51
54
  # When using schemes that don't require the client_secret to be passed i.e TLS Client Auth,
52
55
  # we don't want to send the secret
53
56
  def apply_client_id(params)
54
- {'client_id' => id}.merge(params)
57
+ result = {}
58
+ result['client_id'] = id unless id.nil?
59
+ result.merge(params)
55
60
  end
56
61
 
57
62
  # Adds an `Authorization` header with Basic Auth credentials if and only if
@@ -59,7 +64,7 @@ module OAuth2
59
64
  def apply_basic_auth(params)
60
65
  headers = params.fetch(:headers, {})
61
66
  headers = basic_auth_header.merge(headers)
62
- params.merge(:headers => headers)
67
+ params.merge(headers: headers)
63
68
  end
64
69
 
65
70
  # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
data/lib/oauth2/client.rb CHANGED
@@ -5,9 +5,11 @@ require 'logger'
5
5
 
6
6
  module OAuth2
7
7
  ConnectionError = Class.new(Faraday::ConnectionFailed)
8
+ TimeoutError = Class.new(Faraday::TimeoutError)
9
+
8
10
  # The OAuth2::Client class
9
11
  class Client # rubocop:disable Metrics/ClassLength
10
- RESERVED_PARAM_KEYS = %w[headers parse].freeze
12
+ RESERVED_PARAM_KEYS = %w[body headers params parse snaky].freeze
11
13
 
12
14
  attr_reader :id, :secret, :site
13
15
  attr_accessor :options
@@ -22,15 +24,16 @@ module OAuth2
22
24
  # @param [Hash] options the options to create the client with
23
25
  # @option options [String] :site the OAuth2 provider site host
24
26
  # @option options [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange
25
- # @option options [String] :authorize_url ('oauth/authorize') absolute or relative URL path to the Authorization endpoint
26
- # @option options [String] :token_url ('oauth/token') absolute or relative URL path to the Token endpoint
27
- # @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get or :post)
27
+ # @option options [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint
28
+ # @option options [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint
29
+ # @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get, :post, :post_with_query_string)
28
30
  # @option options [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
29
31
  # @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
30
32
  # @option options [FixNum] :max_redirects (5) maximum number of redirects to follow
31
33
  # @option options [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
32
34
  # @option options [Logger] :logger (::Logger.new($stdout)) which logger to use when OAUTH_DEBUG is enabled
33
- # @option options [Proc] (DEPRECATED) :extract_access_token proc that extracts the access token from the response
35
+ # @option options [Proc] :extract_access_token proc that takes the client and the response Hash and extracts the access token from the response (DEPRECATED)
36
+ # @option options [Class] :access_token_class [Class] class of access token for easier subclassing OAuth2::AccessToken, @version 2.0+
34
37
  # @yield [builder] The Faraday connection builder
35
38
  def initialize(client_id, client_secret, options = {}, &block)
36
39
  opts = options.dup
@@ -38,16 +41,19 @@ module OAuth2
38
41
  @secret = client_secret
39
42
  @site = opts.delete(:site)
40
43
  ssl = opts.delete(:ssl)
41
- @options = {:authorize_url => 'oauth/authorize',
42
- :token_url => 'oauth/token',
43
- :token_method => :post,
44
- :auth_scheme => :request_body,
45
- :connection_opts => {},
46
- :connection_build => block,
47
- :max_redirects => 5,
48
- :raise_errors => true,
49
- :extract_access_token => DEFAULT_EXTRACT_ACCESS_TOKEN, # DEPRECATED
50
- :logger => ::Logger.new($stdout)}.merge(opts)
44
+ warn('OAuth2::Client#initialize argument `extract_access_token` will be removed in oauth2 v3. Refactor to use `access_token_class`.') if opts[:extract_access_token]
45
+ @options = {
46
+ authorize_url: 'oauth/authorize',
47
+ token_url: 'oauth/token',
48
+ token_method: :post,
49
+ auth_scheme: :basic_auth,
50
+ connection_opts: {},
51
+ connection_build: block,
52
+ max_redirects: 5,
53
+ raise_errors: true,
54
+ logger: ::Logger.new($stdout),
55
+ access_token_class: AccessToken,
56
+ }.merge(opts)
51
57
  @options[:connection_opts][:ssl] = ssl if ssl
52
58
  end
53
59
 
@@ -89,6 +95,9 @@ module OAuth2
89
95
  end
90
96
 
91
97
  # Makes a request relative to the specified site root.
98
+ # Updated HTTP 1.1 specification (IETF RFC 7231) relaxed the original constraint (IETF RFC 2616),
99
+ # allowing the use of relative URLs in Location headers.
100
+ # @see https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2
92
101
  #
93
102
  # @param [Symbol] verb one of :get, :post, :put, :delete
94
103
  # @param [String] url URL path of request
@@ -99,20 +108,10 @@ module OAuth2
99
108
  # @option opts [Boolean] :raise_errors whether or not to raise an OAuth2::Error on 400+ status
100
109
  # code response for this request. Will default to client option
101
110
  # @option opts [Symbol] :parse @see Response::initialize
102
- # @yield [req] The Faraday request
103
- def request(verb, url, opts = {}) # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
104
- url = connection.build_url(url).to_s
105
-
106
- begin
107
- response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
108
- req.params.update(opts[:params]) if opts[:params]
109
- yield(req) if block_given?
110
- end
111
- rescue Faraday::ConnectionFailed => e
112
- raise ConnectionError, e
113
- end
114
-
115
- response = Response.new(response, :parse => opts[:parse])
111
+ # @option opts [true, false] :snaky (true) @see Response::initialize
112
+ # @yield [req] @see Faraday::Connection#run_request
113
+ def request(verb, url, opts = {}, &block)
114
+ response = execute_request(verb, url, opts, &block)
116
115
 
117
116
  case response.status
118
117
  when 301, 302, 303, 307
@@ -126,7 +125,8 @@ module OAuth2
126
125
  end
127
126
  location = response.headers['location']
128
127
  if location
129
- request(verb, location, opts)
128
+ full_location = response.response.env.url.merge(location)
129
+ request(verb, full_location, opts)
130
130
  else
131
131
  error = Error.new(response)
132
132
  raise(error, "Got #{response.status} status code, but no Location header was present")
@@ -138,7 +138,6 @@ module OAuth2
138
138
  error = Error.new(response)
139
139
  raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
140
140
 
141
- response.error = error
142
141
  response
143
142
  else
144
143
  error = Error.new(response)
@@ -148,53 +147,54 @@ module OAuth2
148
147
 
149
148
  # Initializes an AccessToken by making a request to the token endpoint
150
149
  #
151
- # @param params [Hash] a Hash of params for the token endpoint
150
+ # @param params [Hash] a Hash of params for the token endpoint, except:
151
+ # @option params [Symbol] :parse @see Response#initialize
152
+ # @option params [true, false] :snaky (true) @see Response#initialize
152
153
  # @param access_token_opts [Hash] access token options, to pass to the AccessToken object
153
- # @param access_token_class [Class] class of access token for easier subclassing OAuth2::AccessToken
154
+ # @param extract_access_token [Proc] proc that extracts the access token from the response (DEPRECATED)
155
+ # @yield [req] @see Faraday::Connection#run_request
154
156
  # @return [AccessToken] the initialized AccessToken
155
- def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token]) # # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity Metrics/AbcSize, Metrics/MethodLength
157
+ def get_token(params, access_token_opts = {}, extract_access_token = nil, &block)
158
+ warn('OAuth2::Client#get_token argument `extract_access_token` will be removed in oauth2 v3. Refactor to use `access_token_class` on #initialize.') if extract_access_token
159
+ extract_access_token ||= options[:extract_access_token]
156
160
  params = params.map do |key, value|
157
161
  if RESERVED_PARAM_KEYS.include?(key)
158
162
  [key.to_sym, value]
159
163
  else
160
164
  [key, value]
161
165
  end
162
- end
163
- params = Hash[params]
166
+ end.to_h
167
+
168
+ parse = params.key?(:parse) ? params.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
169
+ snaky = params.key?(:snaky) ? params.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
164
170
 
171
+ request_opts = {
172
+ raise_errors: options[:raise_errors],
173
+ parse: parse,
174
+ snaky: snaky,
175
+ }
165
176
  params = authenticator.apply(params)
166
- opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
167
177
  headers = params.delete(:headers) || {}
168
178
  if options[:token_method] == :post
169
- opts[:body] = params
170
- opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
179
+ request_opts[:body] = params
180
+ request_opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
171
181
  else
172
- opts[:params] = params
173
- opts[:headers] = {}
182
+ request_opts[:params] = params
183
+ request_opts[:headers] = {}
174
184
  end
175
- opts[:headers] = opts[:headers].merge(headers)
185
+ request_opts[:headers].merge!(headers)
176
186
  http_method = options[:token_method]
177
- response = request(http_method, token_url, opts)
178
-
179
- access_token = begin
180
- build_access_token(response, access_token_opts, extract_access_token)
181
- rescue StandardError
182
- nil
183
- end
184
-
185
- response_contains_token = access_token || (
186
- response.parsed.is_a?(Hash) &&
187
- (response.parsed['access_token'] || response.parsed['id_token'])
188
- )
189
-
190
- if options[:raise_errors] && !response_contains_token
191
- error = Error.new(response)
192
- raise(error)
193
- elsif !response_contains_token
194
- return nil
187
+ http_method = :post if http_method == :post_with_query_string
188
+ response = request(http_method, token_url, request_opts, &block)
189
+
190
+ # In v1.4.x, the deprecated extract_access_token option retrieves the token from the response.
191
+ # We preserve this behavior here, but a custom access_token_class that implements #from_hash
192
+ # should be used instead.
193
+ if extract_access_token
194
+ parse_response_with_legacy_extract(response, access_token_opts, extract_access_token)
195
+ else
196
+ parse_response(response, access_token_opts)
195
197
  end
196
-
197
- access_token
198
198
  end
199
199
 
200
200
  # The Authorization Code strategy
@@ -253,13 +253,28 @@ module OAuth2
253
253
  end
254
254
  end
255
255
 
256
- DEFAULT_EXTRACT_ACCESS_TOKEN = proc do |client, hash|
257
- token = hash.delete('access_token') || hash.delete(:access_token)
258
- token && AccessToken.new(client, token, hash)
259
- end
260
-
261
256
  private
262
257
 
258
+ def execute_request(verb, url, opts = {})
259
+ url = connection.build_url(url).to_s
260
+
261
+ begin
262
+ response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
263
+ req.params.update(opts[:params]) if opts[:params]
264
+ yield(req) if block_given?
265
+ end
266
+ rescue Faraday::ConnectionFailed => e
267
+ raise ConnectionError, e
268
+ rescue Faraday::TimeoutError => e
269
+ raise TimeoutError, e
270
+ end
271
+
272
+ parse = opts.key?(:parse) ? opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
273
+ snaky = opts.key?(:snaky) ? opts.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
274
+
275
+ Response.new(response, parse: parse, snaky: snaky)
276
+ end
277
+
263
278
  # Returns the authenticator object
264
279
  #
265
280
  # @return [Authenticator] the initialized Authenticator
@@ -267,26 +282,53 @@ module OAuth2
267
282
  Authenticator.new(id, secret, options[:auth_scheme])
268
283
  end
269
284
 
285
+ def parse_response_with_legacy_extract(response, access_token_opts, extract_access_token)
286
+ access_token = build_access_token_legacy_extract(response, access_token_opts, extract_access_token)
287
+
288
+ return access_token if access_token
289
+
290
+ if options[:raise_errors]
291
+ error = Error.new(response)
292
+ raise(error)
293
+ end
294
+
295
+ nil
296
+ end
297
+
298
+ def parse_response(response, access_token_opts)
299
+ access_token_class = options[:access_token_class]
300
+ data = response.parsed
301
+
302
+ unless data.is_a?(Hash) && !data.empty?
303
+ return unless options[:raise_errors]
304
+
305
+ error = Error.new(response)
306
+ raise(error)
307
+ end
308
+
309
+ build_access_token(response, access_token_opts, access_token_class)
310
+ end
311
+
270
312
  # Builds the access token from the response of the HTTP call
271
313
  #
272
314
  # @return [AccessToken] the initialized AccessToken
273
- def build_access_token(response, access_token_opts, extract_access_token)
274
- parsed_response = response.parsed.dup
275
- return unless parsed_response.is_a?(Hash)
276
-
277
- hash = parsed_response.merge(access_token_opts)
278
-
279
- # Provide backwards compatibility for old AccessToken.form_hash pattern
280
- # Will be deprecated in 2.x
281
- if extract_access_token.is_a?(Class) && extract_access_token.respond_to?(:from_hash)
282
- extract_access_token.from_hash(self, hash)
283
- else
284
- extract_access_token.call(self, hash)
315
+ def build_access_token(response, access_token_opts, access_token_class)
316
+ access_token_class.from_hash(self, response.parsed.merge(access_token_opts)).tap do |access_token|
317
+ access_token.response = response if access_token.respond_to?(:response=)
285
318
  end
286
319
  end
287
320
 
321
+ # Builds the access token from the response of the HTTP call with legacy extract_access_token
322
+ #
323
+ # @return [AccessToken] the initialized AccessToken
324
+ def build_access_token_legacy_extract(response, access_token_opts, extract_access_token)
325
+ extract_access_token.call(self, response.parsed.merge(access_token_opts))
326
+ rescue StandardError
327
+ nil
328
+ end
329
+
288
330
  def oauth_debug_logging(builder)
289
- builder.response :logger, options[:logger], :bodies => true if ENV['OAUTH_DEBUG'] == 'true'
331
+ builder.response :logger, options[:logger], bodies: true if ENV['OAUTH_DEBUG'] == 'true'
290
332
  end
291
333
  end
292
334
  end
data/lib/oauth2/error.rb CHANGED
@@ -2,41 +2,58 @@
2
2
 
3
3
  module OAuth2
4
4
  class Error < StandardError
5
- attr_reader :response, :code, :description
5
+ attr_reader :response, :body, :code, :description
6
6
 
7
- # standard error values include:
8
- # :invalid_request, :invalid_client, :invalid_token, :invalid_grant, :unsupported_grant_type, :invalid_scope
7
+ # standard error codes include:
8
+ # 'invalid_request', 'invalid_client', 'invalid_token', 'invalid_grant', 'unsupported_grant_type', 'invalid_scope'
9
+ # response might be a Response object, or the response.parsed hash
9
10
  def initialize(response)
10
- response.error = self
11
11
  @response = response
12
-
13
- if response.parsed.is_a?(Hash)
14
- @code = response.parsed['error']
15
- @description = response.parsed['error_description']
16
- error_description = "#{@code}: #{@description}"
12
+ if response.respond_to?(:parsed)
13
+ if response.parsed.is_a?(Hash)
14
+ @code = response.parsed['error']
15
+ @description = response.parsed['error_description']
16
+ end
17
+ elsif response.is_a?(Hash)
18
+ @code = response['error']
19
+ @description = response['error_description']
17
20
  end
18
-
19
- super(error_message(response.body, :error_description => error_description))
21
+ @body = if response.respond_to?(:body)
22
+ response.body
23
+ else
24
+ @response
25
+ end
26
+ message_opts = parse_error_description(@code, @description)
27
+ super(error_message(@body, message_opts))
20
28
  end
21
29
 
22
- # Makes a error message
23
- # @param [String] response_body response body of request
24
- # @param [String] opts :error_description error description to show first line
30
+ private
31
+
25
32
  def error_message(response_body, opts = {})
26
- message = []
33
+ lines = []
34
+
35
+ lines << opts[:error_description] if opts[:error_description]
27
36
 
28
- opts[:error_description] && (message << opts[:error_description])
37
+ error_string = if response_body.respond_to?(:encode) && opts[:error_description].respond_to?(:encoding)
38
+ script_encoding = opts[:error_description].encoding
39
+ response_body.encode(script_encoding, invalid: :replace, undef: :replace)
40
+ else
41
+ response_body
42
+ end
43
+
44
+ lines << error_string
45
+
46
+ lines.join("\n")
47
+ end
29
48
 
30
- error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding)
31
- script_encoding = opts[:error_description].encoding
32
- response_body.encode(script_encoding, :invalid => :replace, :undef => :replace)
33
- else
34
- response_body
35
- end
49
+ def parse_error_description(code, description)
50
+ return {} unless code || description
36
51
 
37
- message << error_message
52
+ error_description = ''
53
+ error_description += "#{code}: " if code
54
+ error_description += description if description
38
55
 
39
- message.join("\n")
56
+ {error_description: error_description}
40
57
  end
41
58
  end
42
59
  end