zendesk_api 3.1.1 → 4.0.0.pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/zendesk_api/actions.rb +31 -31
- data/lib/zendesk_api/association.rb +8 -8
- data/lib/zendesk_api/associations.rb +30 -30
- data/lib/zendesk_api/client.rb +69 -36
- data/lib/zendesk_api/collection.rb +41 -40
- data/lib/zendesk_api/configuration.rb +11 -8
- data/lib/zendesk_api/core_ext/inflection.rb +2 -2
- data/lib/zendesk_api/delegator.rb +1 -1
- data/lib/zendesk_api/helpers.rb +10 -10
- data/lib/zendesk_api/middleware/request/api_token_impersonate.rb +29 -0
- data/lib/zendesk_api/middleware/request/encode_json.rb +3 -4
- data/lib/zendesk_api/middleware/request/etag_cache.rb +27 -4
- data/lib/zendesk_api/middleware/request/raise_rate_limited.rb +3 -3
- data/lib/zendesk_api/middleware/request/retry.rb +56 -20
- data/lib/zendesk_api/middleware/request/upload.rb +1 -1
- data/lib/zendesk_api/middleware/request/url_based_access_token.rb +2 -2
- data/lib/zendesk_api/middleware/response/deflate.rb +1 -1
- data/lib/zendesk_api/middleware/response/gzip.rb +3 -3
- data/lib/zendesk_api/middleware/response/logger.rb +3 -3
- data/lib/zendesk_api/middleware/response/parse_iso_dates.rb +1 -1
- data/lib/zendesk_api/middleware/response/parse_json.rb +3 -3
- data/lib/zendesk_api/middleware/response/raise_error.rb +1 -1
- data/lib/zendesk_api/middleware/response/sanitize_response.rb +1 -1
- data/lib/zendesk_api/middleware/response/zendesk_request_event.rb +72 -0
- data/lib/zendesk_api/pagination.rb +3 -3
- data/lib/zendesk_api/resource.rb +16 -24
- data/lib/zendesk_api/resources.rb +84 -80
- data/lib/zendesk_api/search.rb +6 -6
- data/lib/zendesk_api/silent_mash.rb +1 -1
- data/lib/zendesk_api/track_changes.rb +2 -2
- data/lib/zendesk_api/trackie.rb +3 -3
- data/lib/zendesk_api/version.rb +1 -1
- data/lib/zendesk_api.rb +5 -5
- data/util/resource_handler.rb +5 -5
- data/util/verb_handler.rb +1 -1
- metadata +27 -16
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
require_relative "resource"
|
|
2
|
+
require_relative "resources"
|
|
3
|
+
require_relative "search"
|
|
4
|
+
require_relative "pagination"
|
|
5
5
|
|
|
6
6
|
module ZendeskAPI
|
|
7
7
|
# Represents a collection of resources. Lazily loaded, resources aren't
|
|
@@ -50,8 +50,8 @@ module ZendeskAPI
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Methods that take a Hash argument
|
|
53
|
-
methods = %w
|
|
54
|
-
methods += methods.map { |method| method
|
|
53
|
+
methods = %w[create find update update_many destroy create_or_update]
|
|
54
|
+
methods += methods.map { |method| "#{method}!" }
|
|
55
55
|
methods.each do |deferrable|
|
|
56
56
|
# Passes arguments and the proper path to the resource class method.
|
|
57
57
|
# @param [Hash] options Options or attributes to pass
|
|
@@ -61,14 +61,14 @@ module ZendeskAPI
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
args << {} unless args.last.is_a?(Hash)
|
|
64
|
-
args.last
|
|
64
|
+
args.last[:association] = @association
|
|
65
65
|
|
|
66
66
|
@resource_class.send(deferrable, @client, *args)
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# Methods that take an Array argument
|
|
71
|
-
methods = %w
|
|
71
|
+
methods = %w[create_many! destroy_many!]
|
|
72
72
|
methods.each do |deferrable|
|
|
73
73
|
# Passes arguments and the proper path to the resource class method.
|
|
74
74
|
# @param [Array] array arguments
|
|
@@ -153,7 +153,7 @@ module ZendeskAPI
|
|
|
153
153
|
|
|
154
154
|
# The API path to this collection
|
|
155
155
|
def path
|
|
156
|
-
@association.generate_path(:
|
|
156
|
+
@association.generate_path(with_parent: true)
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
# Executes actual GET from API and loads resources into proper class.
|
|
@@ -161,7 +161,7 @@ module ZendeskAPI
|
|
|
161
161
|
def fetch!(reload = false)
|
|
162
162
|
if @resources && (!@fetchable || !reload)
|
|
163
163
|
return @resources
|
|
164
|
-
elsif association
|
|
164
|
+
elsif association&.options&.parent&.new_record?
|
|
165
165
|
return (@resources = [])
|
|
166
166
|
end
|
|
167
167
|
|
|
@@ -188,24 +188,24 @@ module ZendeskAPI
|
|
|
188
188
|
|
|
189
189
|
# Calls #each on every page with the passed in block
|
|
190
190
|
# @param [Block] block Passed to #each
|
|
191
|
-
def all!(start_page = @options["page"], &
|
|
192
|
-
_all(start_page, :bang, &
|
|
191
|
+
def all!(start_page = @options["page"], &)
|
|
192
|
+
_all(start_page, :bang, &)
|
|
193
193
|
end
|
|
194
194
|
|
|
195
195
|
# Calls #each on every page with the passed in block
|
|
196
196
|
# @param [Block] block Passed to #each
|
|
197
|
-
def all(start_page = @options["page"], &
|
|
198
|
-
_all(start_page, &
|
|
197
|
+
def all(start_page = @options["page"], &)
|
|
198
|
+
_all(start_page, &)
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
-
def each_page!(
|
|
201
|
+
def each_page!(...)
|
|
202
202
|
warn "ZendeskAPI::Collection#each_page! is deprecated, please use ZendeskAPI::Collection#all!"
|
|
203
|
-
all!(
|
|
203
|
+
all!(...)
|
|
204
204
|
end
|
|
205
205
|
|
|
206
|
-
def each_page(
|
|
206
|
+
def each_page(...)
|
|
207
207
|
warn "ZendeskAPI::Collection#each_page is deprecated, please use ZendeskAPI::Collection#all"
|
|
208
|
-
all(
|
|
208
|
+
all(...)
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
# Replaces the current (loaded or not) resources with the passed in collection
|
|
@@ -271,13 +271,13 @@ module ZendeskAPI
|
|
|
271
271
|
end
|
|
272
272
|
|
|
273
273
|
# Sends methods to underlying array of resources.
|
|
274
|
-
def method_missing(name,
|
|
274
|
+
def method_missing(name, ...)
|
|
275
275
|
if resource_methods.include?(name)
|
|
276
|
-
collection_method(name,
|
|
276
|
+
collection_method(name, ...)
|
|
277
277
|
elsif [].respond_to?(name, false)
|
|
278
|
-
array_method(name,
|
|
278
|
+
array_method(name, ...)
|
|
279
279
|
else
|
|
280
|
-
next_collection(name,
|
|
280
|
+
next_collection(name, ...)
|
|
281
281
|
end
|
|
282
282
|
end
|
|
283
283
|
|
|
@@ -289,11 +289,11 @@ module ZendeskAPI
|
|
|
289
289
|
inspect = []
|
|
290
290
|
inspect << "options=#{@options.inspect}" if @options.any?
|
|
291
291
|
inspect << "path=#{path}"
|
|
292
|
-
"#{Inflection.singular(@resource)} collection [#{inspect.join(
|
|
292
|
+
"#{Inflection.singular(@resource)} collection [#{inspect.join(",")}]"
|
|
293
293
|
end
|
|
294
294
|
end
|
|
295
295
|
|
|
296
|
-
|
|
296
|
+
alias_method :to_str, :to_s
|
|
297
297
|
|
|
298
298
|
def to_param
|
|
299
299
|
map(&:to_param)
|
|
@@ -303,7 +303,7 @@ module ZendeskAPI
|
|
|
303
303
|
link = original_response_body["links"]["next"]
|
|
304
304
|
result_key = @resource_class.model_key || "results"
|
|
305
305
|
while link
|
|
306
|
-
response = @client.connection.send(
|
|
306
|
+
response = @client.connection.send(:get, link).body
|
|
307
307
|
|
|
308
308
|
original_response_body[result_key] = original_response_body[result_key] + response[result_key]
|
|
309
309
|
|
|
@@ -338,7 +338,7 @@ module ZendeskAPI
|
|
|
338
338
|
page(start_page)
|
|
339
339
|
clear_cache
|
|
340
340
|
|
|
341
|
-
while
|
|
341
|
+
while bang ? fetch! : fetch
|
|
342
342
|
each do |resource|
|
|
343
343
|
block.call(resource, @options["page"] || 1)
|
|
344
344
|
end
|
|
@@ -370,7 +370,7 @@ module ZendeskAPI
|
|
|
370
370
|
# some params use comma-joined strings instead of query-based arrays for multiple values
|
|
371
371
|
@options.each do |k, v|
|
|
372
372
|
if SPECIALLY_JOINED_PARAMS.include?(k.to_sym) && v.is_a?(Array)
|
|
373
|
-
@options[k] = v.join(
|
|
373
|
+
@options[k] = v.join(",")
|
|
374
374
|
end
|
|
375
375
|
end
|
|
376
376
|
end
|
|
@@ -378,9 +378,9 @@ module ZendeskAPI
|
|
|
378
378
|
def set_association_from_options
|
|
379
379
|
@collection_path = @options.delete(:collection_path)
|
|
380
380
|
|
|
381
|
-
association_options = {
|
|
381
|
+
association_options = {path: @options.delete(:path)}
|
|
382
382
|
association_options[:path] ||= @collection_path.join("/") if @collection_path
|
|
383
|
-
@association = @options.delete(:association) || Association.new(association_options.merge(:
|
|
383
|
+
@association = @options.delete(:association) || Association.new(association_options.merge(class: @resource_class))
|
|
384
384
|
@collection_path ||= [@resource]
|
|
385
385
|
end
|
|
386
386
|
|
|
@@ -389,9 +389,9 @@ module ZendeskAPI
|
|
|
389
389
|
@client.connection.send(@verb || "get", path) do |req|
|
|
390
390
|
opts = @options.delete_if { |_, v| v.nil? }
|
|
391
391
|
|
|
392
|
-
req.params
|
|
392
|
+
req.params[:include] = @includes.join(",") if @includes.any?
|
|
393
393
|
|
|
394
|
-
if %w
|
|
394
|
+
if %w[put post].include?(@verb.to_s)
|
|
395
395
|
req.body = opts
|
|
396
396
|
else
|
|
397
397
|
req.params.merge!(opts)
|
|
@@ -440,11 +440,11 @@ module ZendeskAPI
|
|
|
440
440
|
when Array
|
|
441
441
|
wrap_resource(Hash[*res], with_association)
|
|
442
442
|
when Hash
|
|
443
|
-
res = res.merge(:
|
|
443
|
+
res = res.merge(association: @association) if with_association
|
|
444
444
|
@resource_class.new(@client, res)
|
|
445
445
|
else
|
|
446
|
-
res = {
|
|
447
|
-
res
|
|
446
|
+
res = {id: res}
|
|
447
|
+
res[:association] = @association if with_association
|
|
448
448
|
@resource_class.new(@client, res)
|
|
449
449
|
end
|
|
450
450
|
end
|
|
@@ -457,22 +457,23 @@ module ZendeskAPI
|
|
|
457
457
|
|
|
458
458
|
## Method missing
|
|
459
459
|
|
|
460
|
-
def array_method(name,
|
|
461
|
-
to_a.public_send(name,
|
|
460
|
+
def array_method(name, ...)
|
|
461
|
+
to_a.public_send(name, ...)
|
|
462
462
|
end
|
|
463
463
|
|
|
464
464
|
# If you call client.tickets.foo - and foo is not an attribute nor an association, it ends up here, as a new collection
|
|
465
|
-
def next_collection(name, *args, &
|
|
465
|
+
def next_collection(name, *args, &)
|
|
466
466
|
opts = args.last.is_a?(Hash) ? args.last : {}
|
|
467
|
-
opts
|
|
467
|
+
opts[:collection_path] = [*@collection_path, name]
|
|
468
|
+
opts[:page] = nil
|
|
468
469
|
# Why `page: nil`?
|
|
469
470
|
# when you do client.tickets.fetch followed by client.tickets.foos => the request to /tickets/foos will
|
|
470
471
|
# have the options page set to whatever the last options were for the tickets collection
|
|
471
472
|
self.class.new(@client, @resource_class, @options.merge(opts))
|
|
472
473
|
end
|
|
473
474
|
|
|
474
|
-
def collection_method(name,
|
|
475
|
-
@resource_class.send(name, @client,
|
|
475
|
+
def collection_method(name, ...)
|
|
476
|
+
@resource_class.send(name, @client, ...)
|
|
476
477
|
end
|
|
477
478
|
|
|
478
479
|
def resource_methods
|
|
@@ -54,6 +54,9 @@ module ZendeskAPI
|
|
|
54
54
|
# specify if you want a (network layer) exception to elicit a retry
|
|
55
55
|
attr_accessor :retry_on_exception
|
|
56
56
|
|
|
57
|
+
# specify if you want instrumentation to be used
|
|
58
|
+
attr_accessor :instrumentation
|
|
59
|
+
|
|
57
60
|
def initialize
|
|
58
61
|
@client_options = {}
|
|
59
62
|
@use_resource_cache = true
|
|
@@ -66,16 +69,16 @@ module ZendeskAPI
|
|
|
66
69
|
# @return [Hash] Faraday-formatted hash of options.
|
|
67
70
|
def options
|
|
68
71
|
{
|
|
69
|
-
:
|
|
70
|
-
:
|
|
71
|
-
:
|
|
72
|
-
:
|
|
72
|
+
headers: {
|
|
73
|
+
accept: "application/json",
|
|
74
|
+
accept_encoding: "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
|
|
75
|
+
user_agent: "ZendeskAPI Ruby #{ZendeskAPI::VERSION}"
|
|
73
76
|
},
|
|
74
|
-
:
|
|
75
|
-
:
|
|
76
|
-
:
|
|
77
|
+
request: {
|
|
78
|
+
open_timeout: 10,
|
|
79
|
+
timeout: 60
|
|
77
80
|
},
|
|
78
|
-
:
|
|
81
|
+
url: @url
|
|
79
82
|
}.merge(client_options)
|
|
80
83
|
end
|
|
81
84
|
end
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
require
|
|
1
|
+
require "inflection"
|
|
2
2
|
|
|
3
|
-
Inflection.plural_rule
|
|
3
|
+
Inflection.plural_rule "forum", "forums"
|
data/lib/zendesk_api/helpers.rb
CHANGED
|
@@ -21,10 +21,10 @@ module ZendeskAPI
|
|
|
21
21
|
# @return [string] a string that can become a class, `Module::ClassName`
|
|
22
22
|
def self.modulize_string(string)
|
|
23
23
|
# gsub('__','/'). # why was this ever here?
|
|
24
|
-
string.gsub(/__(.?)/) { "::#{$1.upcase}" }
|
|
25
|
-
gsub(/\/(.?)/) { "::#{$1.upcase}" }
|
|
26
|
-
gsub(/(?:_+|-+)([a-z])/) { $1.upcase }
|
|
27
|
-
gsub(/(\A|\s)([a-z])/) { $1 + $2.upcase }
|
|
24
|
+
string.gsub(/__(.?)/) { "::#{$1.upcase}" }
|
|
25
|
+
.gsub(/\/(.?)/) { "::#{$1.upcase}" }
|
|
26
|
+
.gsub(/(?:_+|-+)([a-z])/) { $1.upcase }
|
|
27
|
+
.gsub(/(\A|\s)([a-z])/) { $1 + $2.upcase }
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
# From https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/snakecase.rb
|
|
@@ -38,12 +38,12 @@ module ZendeskAPI
|
|
|
38
38
|
# "Snake - Case".snakecase #=> "snake_case"
|
|
39
39
|
def self.snakecase_string(string)
|
|
40
40
|
# gsub(/::/, '/').
|
|
41
|
-
string.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
42
|
-
gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
43
|
-
tr(
|
|
44
|
-
gsub(/\s/,
|
|
45
|
-
gsub(/__+/,
|
|
46
|
-
downcase
|
|
41
|
+
string.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
42
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
43
|
+
.tr("-", "_")
|
|
44
|
+
.gsub(/\s/, "_")
|
|
45
|
+
.gsub(/__+/, "_")
|
|
46
|
+
.downcase
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
|
|
3
|
+
module ZendeskAPI
|
|
4
|
+
# @private
|
|
5
|
+
module Middleware
|
|
6
|
+
# @private
|
|
7
|
+
module Request
|
|
8
|
+
# ApiTokenImpersonate
|
|
9
|
+
# If Thread.current[:zendesk_thread_local_username] is set, it will modify the Authorization header
|
|
10
|
+
# to impersonate that user using the API token from the current Authorization header.
|
|
11
|
+
class ApiTokenImpersonate < Faraday::Middleware
|
|
12
|
+
def call(env)
|
|
13
|
+
if Thread.current[:zendesk_thread_local_username] && env[:request_headers][:authorization] =~ /^Basic /
|
|
14
|
+
current_u_p_encoded = env[:request_headers][:authorization].split(/\s+/)[1]
|
|
15
|
+
current_u_p = Base64.urlsafe_decode64(current_u_p_encoded)
|
|
16
|
+
unless current_u_p.include?("/token:") && (parts = current_u_p.split(":")) && parts.length == 2 && parts[0].include?("/token")
|
|
17
|
+
warn "WARNING: ApiTokenImpersonate passed in invalid format. It should be in the format username/token:APITOKEN"
|
|
18
|
+
return @app.call(env)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
next_u_p = "#{Thread.current[:zendesk_thread_local_username]}/token:#{parts[1]}"
|
|
22
|
+
env[:request_headers][:authorization] = "Basic #{Base64.urlsafe_encode64(next_u_p)}"
|
|
23
|
+
end
|
|
24
|
+
@app.call(env)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -4,13 +4,12 @@ module ZendeskAPI
|
|
|
4
4
|
# @private
|
|
5
5
|
module Request
|
|
6
6
|
class EncodeJson < Faraday::Middleware
|
|
7
|
-
CONTENT_TYPE =
|
|
8
|
-
MIME_TYPE =
|
|
7
|
+
CONTENT_TYPE = "Content-Type".freeze
|
|
8
|
+
MIME_TYPE = "application/json".freeze
|
|
9
9
|
|
|
10
10
|
def call(env)
|
|
11
11
|
type = env[:request_headers][CONTENT_TYPE].to_s
|
|
12
|
-
type = type.split(
|
|
13
|
-
type
|
|
12
|
+
type = type.split(";", 2).first if type.index(";")
|
|
14
13
|
|
|
15
14
|
if env[:body] && !(env[:body].respond_to?(:to_str) && env[:body].empty?) && (type.empty? || type == MIME_TYPE)
|
|
16
15
|
env[:body] = JSON.dump(env[:body])
|
|
@@ -9,6 +9,7 @@ module ZendeskAPI
|
|
|
9
9
|
class EtagCache < Faraday::Middleware
|
|
10
10
|
def initialize(app, options = {})
|
|
11
11
|
@app = app
|
|
12
|
+
@instrumentation = options[:instrumentation] if options[:instrumentation].respond_to?(:instrument)
|
|
12
13
|
@cache = options[:cache] ||
|
|
13
14
|
raise("need :cache option e.g. ActiveSupport::Cache::MemoryStore.new")
|
|
14
15
|
@cache_key_prefix = options.fetch(:cache_key_prefix, :faraday_etags)
|
|
@@ -36,13 +37,35 @@ module ZendeskAPI
|
|
|
36
37
|
env[:response_body] = cached[:response_body]
|
|
37
38
|
|
|
38
39
|
env[:response_headers].merge!(
|
|
39
|
-
:
|
|
40
|
-
:
|
|
41
|
-
:
|
|
42
|
-
:
|
|
40
|
+
etag: cached[:response_headers][:etag],
|
|
41
|
+
content_type: cached[:response_headers][:content_type],
|
|
42
|
+
content_length: cached[:response_headers][:content_length],
|
|
43
|
+
content_encoding: cached[:response_headers][:content_encoding]
|
|
43
44
|
)
|
|
45
|
+
if @instrumentation
|
|
46
|
+
begin
|
|
47
|
+
@instrumentation.instrument("zendesk.cache_hit",
|
|
48
|
+
{
|
|
49
|
+
endpoint: env[:url]&.path,
|
|
50
|
+
status: env[:status]
|
|
51
|
+
})
|
|
52
|
+
rescue
|
|
53
|
+
# Swallow instrumentation errors to maintain cache behavior
|
|
54
|
+
end
|
|
55
|
+
end
|
|
44
56
|
elsif env[:status] == 200 && env[:response_headers]["Etag"] # modified and cacheable
|
|
45
57
|
@cache.write(cache_key(env), env.to_hash)
|
|
58
|
+
if @instrumentation
|
|
59
|
+
begin
|
|
60
|
+
@instrumentation.instrument("zendesk.cache_miss",
|
|
61
|
+
{
|
|
62
|
+
endpoint: env[:url]&.path,
|
|
63
|
+
status: env[:status]
|
|
64
|
+
})
|
|
65
|
+
rescue
|
|
66
|
+
# Swallow instrumentation errors to maintain cache behavior
|
|
67
|
+
end
|
|
68
|
+
end
|
|
46
69
|
end
|
|
47
70
|
end
|
|
48
71
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require
|
|
2
|
-
|
|
1
|
+
require "faraday/middleware"
|
|
2
|
+
require_relative "../../error"
|
|
3
3
|
|
|
4
4
|
module ZendeskAPI
|
|
5
5
|
module Middleware
|
|
@@ -19,7 +19,7 @@ module ZendeskAPI
|
|
|
19
19
|
response = @app.call(env)
|
|
20
20
|
|
|
21
21
|
if ERROR_CODES.include?(response.env[:status])
|
|
22
|
-
@logger&.warn
|
|
22
|
+
@logger&.warn "You have been rate limited. Raising error..."
|
|
23
23
|
raise Error::RateLimited, env
|
|
24
24
|
else
|
|
25
25
|
response
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "faraday/middleware"
|
|
2
|
+
|
|
2
3
|
module ZendeskAPI
|
|
3
4
|
module Middleware
|
|
4
5
|
# @private
|
|
@@ -12,46 +13,81 @@ module ZendeskAPI
|
|
|
12
13
|
def initialize(app, options = {})
|
|
13
14
|
super(app)
|
|
14
15
|
@logger = options[:logger]
|
|
15
|
-
@error_codes = options.key?(:retry_codes) && options[:retry_codes] ? options[:retry_codes] : DEFAULT_ERROR_CODES
|
|
16
|
-
@retry_on_exception = options.key?(:retry_on_exception) && options[:retry_on_exception] ? options[:retry_on_exception] : false
|
|
16
|
+
@error_codes = (options.key?(:retry_codes) && options[:retry_codes]) ? options[:retry_codes] : DEFAULT_ERROR_CODES
|
|
17
|
+
@retry_on_exception = (options.key?(:retry_on_exception) && options[:retry_on_exception]) ? options[:retry_on_exception] : false
|
|
18
|
+
@instrumentation = options[:instrumentation]
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def call(env)
|
|
22
|
+
# Duplicate env for retries but keep attempt counter persistent
|
|
20
23
|
original_env = env.dup
|
|
24
|
+
original_env[:call_attempt] = (env[:call_attempt] || 0)
|
|
25
|
+
|
|
21
26
|
exception_happened = false
|
|
27
|
+
response = nil
|
|
28
|
+
|
|
22
29
|
if @retry_on_exception
|
|
23
30
|
begin
|
|
24
31
|
response = @app.call(env)
|
|
25
|
-
rescue
|
|
32
|
+
rescue => ex
|
|
26
33
|
exception_happened = true
|
|
34
|
+
exception = ex
|
|
27
35
|
end
|
|
28
36
|
else
|
|
37
|
+
# Allow exceptions to propagate normally when not retrying
|
|
29
38
|
response = @app.call(env)
|
|
30
39
|
end
|
|
31
40
|
|
|
32
|
-
if exception_happened
|
|
41
|
+
if exception_happened
|
|
42
|
+
original_env[:call_attempt] += 1
|
|
43
|
+
seconds_left = DEFAULT_RETRY_AFTER.to_i
|
|
44
|
+
@logger&.warn "An exception happened, waiting #{seconds_left} seconds... #{exception}"
|
|
45
|
+
instrument_retry(original_env, "exception", seconds_left)
|
|
46
|
+
sleep_with_logging(seconds_left)
|
|
47
|
+
return @app.call(original_env)
|
|
48
|
+
end
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
# Retry once if response has a retryable error code
|
|
51
|
+
if response && @error_codes.include?(response.env[:status])
|
|
52
|
+
original_env[:call_attempt] += 1
|
|
53
|
+
seconds_left = (response.env[:response_headers][:retry_after] || DEFAULT_RETRY_AFTER).to_i
|
|
54
|
+
@logger&.warn "You may have been rate limited. Retrying in #{seconds_left} seconds..."
|
|
55
|
+
instrument_retry(original_env, (response.env[:status] == 429) ? "rate_limited" : "server_error", seconds_left)
|
|
56
|
+
sleep_with_logging(seconds_left)
|
|
57
|
+
response = @app.call(original_env)
|
|
58
|
+
end
|
|
40
59
|
|
|
41
|
-
|
|
60
|
+
response
|
|
61
|
+
end
|
|
42
62
|
|
|
43
|
-
|
|
44
|
-
sleep 1
|
|
45
|
-
time_left = seconds_left - i
|
|
46
|
-
@logger.warn "#{time_left}..." if time_left > 0 && time_left % 5 == 0 && @logger
|
|
47
|
-
end
|
|
63
|
+
private
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
def instrument_retry(env, reason, delay)
|
|
66
|
+
return unless @instrumentation
|
|
50
67
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
begin
|
|
69
|
+
@instrumentation.instrument(
|
|
70
|
+
"zendesk.retry",
|
|
71
|
+
{
|
|
72
|
+
attempt: env[:call_attempt],
|
|
73
|
+
endpoint: env[:url]&.path,
|
|
74
|
+
method: env[:method],
|
|
75
|
+
reason: reason,
|
|
76
|
+
delay: delay
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
rescue => e
|
|
80
|
+
@logger&.debug("zendesk.retry instrumentation failed: #{e.message}")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def sleep_with_logging(seconds_left)
|
|
85
|
+
seconds_left.times do |i|
|
|
86
|
+
sleep 1
|
|
87
|
+
time_left = seconds_left - i
|
|
88
|
+
@logger&.warn "#{time_left}..." if time_left > 0 && time_left % 5 == 0
|
|
54
89
|
end
|
|
90
|
+
@logger&.warn "" if seconds_left > 0
|
|
55
91
|
end
|
|
56
92
|
end
|
|
57
93
|
end
|
|
@@ -7,7 +7,7 @@ module ZendeskAPI
|
|
|
7
7
|
# @private
|
|
8
8
|
class Deflate < Faraday::Middleware
|
|
9
9
|
def on_complete(env)
|
|
10
|
-
return if env[:response_headers][
|
|
10
|
+
return if env[:response_headers]["content-encoding"] != "deflate"
|
|
11
11
|
return if env.body.strip.empty?
|
|
12
12
|
|
|
13
13
|
env.body = Zlib::Inflate.inflate(env.body)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require
|
|
2
|
-
require
|
|
1
|
+
require "zlib"
|
|
2
|
+
require "stringio"
|
|
3
3
|
|
|
4
4
|
module ZendeskAPI
|
|
5
5
|
# @private
|
|
@@ -9,7 +9,7 @@ module ZendeskAPI
|
|
|
9
9
|
# Faraday middleware to handle content-encoding = gzip
|
|
10
10
|
class Gzip < Faraday::Middleware
|
|
11
11
|
def on_complete(env)
|
|
12
|
-
return if env[:response_headers][
|
|
12
|
+
return if env[:response_headers]["content-encoding"] != "gzip"
|
|
13
13
|
return if env[:body].force_encoding(Encoding::BINARY).strip.empty?
|
|
14
14
|
|
|
15
15
|
env[:body] = Zlib::GzipReader.new(StringIO.new(env[:body])).read
|
|
@@ -10,13 +10,13 @@ module ZendeskAPI
|
|
|
10
10
|
super(app)
|
|
11
11
|
|
|
12
12
|
@logger = logger || begin
|
|
13
|
-
require
|
|
14
|
-
::Logger.new(
|
|
13
|
+
require "logger"
|
|
14
|
+
::Logger.new($stdout)
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def call(env)
|
|
19
|
-
@logger.info "#{env[:method]} #{env[:url]
|
|
19
|
+
@logger.info "#{env[:method]} #{env[:url]}"
|
|
20
20
|
@logger.debug dump_debug(env, :request_headers)
|
|
21
21
|
|
|
22
22
|
@app.call(env).on_complete do |env|
|
|
@@ -4,13 +4,13 @@ module ZendeskAPI
|
|
|
4
4
|
# @private
|
|
5
5
|
module Response
|
|
6
6
|
class ParseJson < Faraday::Middleware
|
|
7
|
-
CONTENT_TYPE =
|
|
7
|
+
CONTENT_TYPE = "Content-Type".freeze
|
|
8
8
|
|
|
9
9
|
def on_complete(env)
|
|
10
10
|
type = env[:response_headers][CONTENT_TYPE].to_s
|
|
11
|
-
type = type.split(
|
|
11
|
+
type = type.split(";", 2).first if type.index(";")
|
|
12
12
|
|
|
13
|
-
return unless type ==
|
|
13
|
+
return unless type == "application/json"
|
|
14
14
|
|
|
15
15
|
unless env[:body].strip.empty?
|
|
16
16
|
env[:body] = JSON.parse(env[:body])
|