kapso-client-ruby 1.0.0 → 1.0.2
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/.rubocop.yml +81 -81
- data/CHANGELOG.md +262 -91
- data/Gemfile +20 -20
- data/RAILS_INTEGRATION.md +478 -0
- data/README.md +1053 -734
- data/Rakefile +40 -40
- data/TEMPLATE_TOOLS_GUIDE.md +120 -120
- data/WHATSAPP_24_HOUR_GUIDE.md +133 -133
- data/examples/advanced_features.rb +352 -349
- data/examples/advanced_messaging.rb +241 -0
- data/examples/basic_messaging.rb +139 -136
- data/examples/enhanced_interactive.rb +400 -0
- data/examples/flows_usage.rb +307 -0
- data/examples/interactive_messages.rb +343 -0
- data/examples/media_management.rb +256 -253
- data/examples/rails/jobs.rb +388 -0
- data/examples/rails/models.rb +240 -0
- data/examples/rails/notifications_controller.rb +227 -0
- data/examples/template_management.rb +393 -390
- data/kapso-ruby-logo.jpg +0 -0
- data/lib/kapso_client_ruby/client.rb +321 -316
- data/lib/kapso_client_ruby/errors.rb +348 -329
- data/lib/kapso_client_ruby/rails/generators/install_generator.rb +76 -0
- data/lib/kapso_client_ruby/rails/generators/templates/env.erb +21 -0
- data/lib/kapso_client_ruby/rails/generators/templates/initializer.rb.erb +33 -0
- data/lib/kapso_client_ruby/rails/generators/templates/message_service.rb.erb +138 -0
- data/lib/kapso_client_ruby/rails/generators/templates/webhook_controller.rb.erb +62 -0
- data/lib/kapso_client_ruby/rails/railtie.rb +55 -0
- data/lib/kapso_client_ruby/rails/service.rb +189 -0
- data/lib/kapso_client_ruby/rails/tasks.rake +167 -0
- data/lib/kapso_client_ruby/resources/calls.rb +172 -172
- data/lib/kapso_client_ruby/resources/contacts.rb +190 -190
- data/lib/kapso_client_ruby/resources/conversations.rb +103 -103
- data/lib/kapso_client_ruby/resources/flows.rb +382 -0
- data/lib/kapso_client_ruby/resources/media.rb +205 -205
- data/lib/kapso_client_ruby/resources/messages.rb +760 -380
- data/lib/kapso_client_ruby/resources/phone_numbers.rb +85 -85
- data/lib/kapso_client_ruby/resources/templates.rb +283 -283
- data/lib/kapso_client_ruby/types.rb +348 -262
- data/lib/kapso_client_ruby/version.rb +5 -5
- data/lib/kapso_client_ruby.rb +75 -68
- data/scripts/.env.example +17 -17
- data/scripts/kapso_template_finder.rb +91 -91
- data/scripts/sdk_setup.rb +404 -404
- data/scripts/test.rb +60 -60
- metadata +24 -3
|
@@ -1,330 +1,349 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module KapsoClientRuby
|
|
4
|
-
module Errors
|
|
5
|
-
# Error categories mapped from the JavaScript implementation
|
|
6
|
-
ERROR_CATEGORIES = {
|
|
7
|
-
'authorization' => :authorization,
|
|
8
|
-
'permission' => :permission,
|
|
9
|
-
'parameter' => :parameter,
|
|
10
|
-
'throttling' => :throttling,
|
|
11
|
-
'template' => :template,
|
|
12
|
-
'media' => :media,
|
|
13
|
-
'phone_registration' => :phone_registration,
|
|
14
|
-
'integrity' => :integrity,
|
|
15
|
-
'business_eligibility' => :business_eligibility,
|
|
16
|
-
'reengagement_window' => :reengagement_window,
|
|
17
|
-
'waba_config' => :waba_config,
|
|
18
|
-
'flow' => :flow,
|
|
19
|
-
'synchronization' => :synchronization,
|
|
20
|
-
'server' => :server,
|
|
21
|
-
'unknown' => :unknown
|
|
22
|
-
}.freeze
|
|
23
|
-
|
|
24
|
-
# Error codes and their categories
|
|
25
|
-
ERROR_CODE_CATEGORIES = {
|
|
26
|
-
0 => :authorization,
|
|
27
|
-
190 => :authorization,
|
|
28
|
-
3 => :permission,
|
|
29
|
-
10 => :permission,
|
|
30
|
-
(200..219) => :permission,
|
|
31
|
-
4 => :throttling,
|
|
32
|
-
80007 => :throttling,
|
|
33
|
-
130429 => :throttling,
|
|
34
|
-
131048 => :throttling,
|
|
35
|
-
131056 => :throttling,
|
|
36
|
-
33 => :parameter,
|
|
37
|
-
100 => :parameter,
|
|
38
|
-
130472 => :parameter,
|
|
39
|
-
131008 => :parameter,
|
|
40
|
-
131009 => :parameter,
|
|
41
|
-
131021 => :parameter,
|
|
42
|
-
131026 => :parameter,
|
|
43
|
-
131051 => :media,
|
|
44
|
-
131052 => :media,
|
|
45
|
-
131053 => :media,
|
|
46
|
-
131000 => :server,
|
|
47
|
-
131016 => :server,
|
|
48
|
-
131057 => :server,
|
|
49
|
-
133004 => :server,
|
|
50
|
-
133005 => :server,
|
|
51
|
-
368 => :integrity,
|
|
52
|
-
130497 => :integrity,
|
|
53
|
-
131031 => :integrity,
|
|
54
|
-
131047 => :reengagement_window,
|
|
55
|
-
131037 => :waba_config,
|
|
56
|
-
131042 => :business_eligibility,
|
|
57
|
-
131045 => :phone_registration,
|
|
58
|
-
133000 => :phone_registration,
|
|
59
|
-
133006 => :phone_registration,
|
|
60
|
-
133008 => :phone_registration,
|
|
61
|
-
133009 => :phone_registration,
|
|
62
|
-
133010 => :phone_registration,
|
|
63
|
-
133015 => :phone_registration,
|
|
64
|
-
133016 => :phone_registration,
|
|
65
|
-
132000 => :template,
|
|
66
|
-
132001 => :template,
|
|
67
|
-
132005 => :template,
|
|
68
|
-
132007 => :template,
|
|
69
|
-
132012 => :template,
|
|
70
|
-
132015 => :template,
|
|
71
|
-
132016 => :template,
|
|
72
|
-
132068 => :flow,
|
|
73
|
-
132069 => :flow,
|
|
74
|
-
134011 => :business_eligibility,
|
|
75
|
-
135000 => :parameter,
|
|
76
|
-
2593107 => :synchronization,
|
|
77
|
-
2593108 => :synchronization
|
|
78
|
-
}.freeze
|
|
79
|
-
|
|
80
|
-
# Error codes that should not be retried
|
|
81
|
-
DO_NOT_RETRY_CODES = [131049, 131050, 131047, 368, 130497, 131031].freeze
|
|
82
|
-
|
|
83
|
-
# Error codes that require token refresh
|
|
84
|
-
REFRESH_TOKEN_CODES = [0, 190].freeze
|
|
85
|
-
|
|
86
|
-
class GraphApiError < StandardError
|
|
87
|
-
attr_reader :http_status, :code, :type, :details, :error_subcode,
|
|
88
|
-
:fbtrace_id, :error_data, :category, :retry_hint, :raw_response, :retry_after
|
|
89
|
-
|
|
90
|
-
def initialize(message: nil, http_status:, code: nil, type: nil, details: nil,
|
|
91
|
-
error_subcode: nil, fbtrace_id: nil, error_data: nil,
|
|
92
|
-
category: nil, retry_hint: nil, raw_response: nil, retry_after: nil)
|
|
93
|
-
@http_status = http_status
|
|
94
|
-
@code = code || http_status
|
|
95
|
-
@type = type || 'GraphApiError'
|
|
96
|
-
@details = details
|
|
97
|
-
@error_subcode = error_subcode
|
|
98
|
-
@fbtrace_id = fbtrace_id
|
|
99
|
-
@error_data = error_data
|
|
100
|
-
@retry_after = retry_after
|
|
101
|
-
@category = category || categorize_error_code(@code, @http_status)
|
|
102
|
-
@retry_hint = retry_hint || derive_retry_hint
|
|
103
|
-
@raw_response = raw_response
|
|
104
|
-
|
|
105
|
-
error_message = message || build_error_message
|
|
106
|
-
super(error_message)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
class << self
|
|
110
|
-
def from_response(response, body = nil, raw_text = nil)
|
|
111
|
-
http_status = response.status
|
|
112
|
-
retry_after_ms = parse_retry_after(response.headers['retry-after'])
|
|
113
|
-
|
|
114
|
-
# Ensure body is a hash for processing
|
|
115
|
-
unless body.is_a?(Hash)
|
|
116
|
-
if body.is_a?(String)
|
|
117
|
-
begin
|
|
118
|
-
body = JSON.parse(body)
|
|
119
|
-
rescue JSON::ParserError
|
|
120
|
-
body = {}
|
|
121
|
-
end
|
|
122
|
-
else
|
|
123
|
-
body = {}
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Check for Graph API error envelope
|
|
128
|
-
if body.key?('error')
|
|
129
|
-
error_payload = body['error']
|
|
130
|
-
code = error_payload['code'] || http_status
|
|
131
|
-
type = error_payload['type'] || 'GraphApiError'
|
|
132
|
-
details = error_payload.is_a?(Hash) ? error_payload.dig('error_data', 'details') : nil
|
|
133
|
-
|
|
134
|
-
new(
|
|
135
|
-
message: error_payload['message'],
|
|
136
|
-
http_status: http_status,
|
|
137
|
-
code: code,
|
|
138
|
-
type: type,
|
|
139
|
-
details: details,
|
|
140
|
-
error_subcode: error_payload['error_subcode'],
|
|
141
|
-
fbtrace_id: error_payload['fbtrace_id'],
|
|
142
|
-
error_data: error_payload['error_data'],
|
|
143
|
-
retry_hint: build_retry_hint_with_delay(code, http_status, retry_after_ms),
|
|
144
|
-
raw_response: body
|
|
145
|
-
)
|
|
146
|
-
elsif body.is_a?(Hash) && body.key?('error') && body['error'].is_a?(String)
|
|
147
|
-
# Kapso proxy error format
|
|
148
|
-
category = http_status >= 500 ? :server : categorize_error_code(nil, http_status)
|
|
149
|
-
new(
|
|
150
|
-
message: body['error'],
|
|
151
|
-
http_status: http_status,
|
|
152
|
-
code: http_status,
|
|
153
|
-
category: category,
|
|
154
|
-
retry_hint: build_retry_hint_with_delay(http_status, http_status, retry_after_ms),
|
|
155
|
-
raw_response: body
|
|
156
|
-
)
|
|
157
|
-
else
|
|
158
|
-
# Generic HTTP error
|
|
159
|
-
category = http_status >= 500 ? :server : categorize_error_code(nil, http_status)
|
|
160
|
-
message = build_default_message(http_status, nil, raw_text)
|
|
161
|
-
|
|
162
|
-
new(
|
|
163
|
-
message: message,
|
|
164
|
-
http_status: http_status,
|
|
165
|
-
code: http_status,
|
|
166
|
-
category: category,
|
|
167
|
-
retry_hint: build_retry_hint_with_delay(http_status, http_status, retry_after_ms),
|
|
168
|
-
raw_response: raw_text || body
|
|
169
|
-
)
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
private
|
|
174
|
-
|
|
175
|
-
def parse_retry_after(header)
|
|
176
|
-
return nil unless header
|
|
177
|
-
|
|
178
|
-
# Try parsing as number of seconds
|
|
179
|
-
if header.match?(/^\d+$/)
|
|
180
|
-
header.to_i * 1000
|
|
181
|
-
else
|
|
182
|
-
# Try parsing as HTTP date
|
|
183
|
-
begin
|
|
184
|
-
date = Time.parse(header)
|
|
185
|
-
diff = (date.to_f - Time.now.to_f) * 1000
|
|
186
|
-
diff > 0 ? diff.to_i : 0
|
|
187
|
-
rescue ArgumentError
|
|
188
|
-
nil
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def categorize_error_code(code, http_status)
|
|
194
|
-
return :authorization if http_status == 401
|
|
195
|
-
return :permission if http_status == 403
|
|
196
|
-
return :parameter if http_status == 404
|
|
197
|
-
return :throttling if http_status == 429
|
|
198
|
-
return :server if http_status >= 500
|
|
199
|
-
return :parameter if http_status >= 400 && http_status < 500
|
|
200
|
-
|
|
201
|
-
if code
|
|
202
|
-
ERROR_CODE_CATEGORIES.each do |key, category|
|
|
203
|
-
if key.is_a?(Range)
|
|
204
|
-
return category if key.include?(code)
|
|
205
|
-
elsif key == code
|
|
206
|
-
return category
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# Check permission range
|
|
211
|
-
return :permission if code >= 200 && code <= 299
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
:unknown
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def build_retry_hint_with_delay(code, http_status, retry_after_ms)
|
|
218
|
-
if retry_after_ms
|
|
219
|
-
{ action: :retry_after, retry_after_ms: retry_after_ms }
|
|
220
|
-
elsif DO_NOT_RETRY_CODES.include?(code)
|
|
221
|
-
{ action: :do_not_retry }
|
|
222
|
-
elsif REFRESH_TOKEN_CODES.include?(code)
|
|
223
|
-
{ action: :refresh_token }
|
|
224
|
-
elsif http_status >= 500
|
|
225
|
-
{ action: :retry }
|
|
226
|
-
else
|
|
227
|
-
{ action: :fix_and_retry }
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def build_default_message(status, details = nil, raw_text = nil)
|
|
232
|
-
if details
|
|
233
|
-
"Meta API request failed with status #{status}: #{details}"
|
|
234
|
-
elsif raw_text && !raw_text.strip.empty?
|
|
235
|
-
"Meta API request failed with status #{status}: #{raw_text}"
|
|
236
|
-
else
|
|
237
|
-
"Meta API request failed with status #{status}"
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def auth_error?
|
|
243
|
-
category == :authorization
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def rate_limit?
|
|
247
|
-
category == :throttling
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def temporary?
|
|
251
|
-
[:throttling, :server, :synchronization].include?(category) ||
|
|
252
|
-
http_status >= 500 ||
|
|
253
|
-
[1, 2, 17, 341].include?(code)
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def template_error?
|
|
257
|
-
category == :template
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def requires_token_refresh?
|
|
261
|
-
category == :authorization || REFRESH_TOKEN_CODES.include?(code)
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def retryable?
|
|
265
|
-
![:do_not_retry].include?(retry_hint[:action])
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def to_h
|
|
269
|
-
{
|
|
270
|
-
name: self.class.name,
|
|
271
|
-
message: message,
|
|
272
|
-
http_status: http_status,
|
|
273
|
-
code: code,
|
|
274
|
-
type: type,
|
|
275
|
-
details: details,
|
|
276
|
-
error_subcode: error_subcode,
|
|
277
|
-
fbtrace_id: fbtrace_id,
|
|
278
|
-
category: category,
|
|
279
|
-
retry_hint: retry_hint,
|
|
280
|
-
raw_response: raw_response
|
|
281
|
-
}
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
private
|
|
285
|
-
|
|
286
|
-
def categorize_error_code(code, http_status)
|
|
287
|
-
self.class.send(:categorize_error_code, code, http_status)
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def derive_retry_hint
|
|
291
|
-
if DO_NOT_RETRY_CODES.include?(code)
|
|
292
|
-
{ action: :do_not_retry }
|
|
293
|
-
elsif REFRESH_TOKEN_CODES.include?(code)
|
|
294
|
-
{ action: :refresh_token }
|
|
295
|
-
elsif http_status >= 500
|
|
296
|
-
{ action: :retry }
|
|
297
|
-
else
|
|
298
|
-
{ action: :fix_and_retry }
|
|
299
|
-
end
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
def build_error_message
|
|
303
|
-
if details
|
|
304
|
-
"Meta API request failed with status #{http_status}: #{details}"
|
|
305
|
-
elsif raw_response.is_a?(String) && !raw_response.strip.empty?
|
|
306
|
-
"Meta API request failed with status #{http_status}: #{raw_response}"
|
|
307
|
-
else
|
|
308
|
-
"Meta API request failed with status #{http_status}"
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
class KapsoProxyRequiredError < StandardError
|
|
314
|
-
attr_reader :feature, :help_url
|
|
315
|
-
|
|
316
|
-
def initialize(feature)
|
|
317
|
-
@feature = feature
|
|
318
|
-
@help_url = 'https://kapso.ai/'
|
|
319
|
-
|
|
320
|
-
message = "#{feature} is only available via the Kapso Proxy. " \
|
|
321
|
-
"Set base_url to https://app.kapso.ai/api/meta and provide kapso_api_key. " \
|
|
322
|
-
"Create a free account at #{help_url}"
|
|
323
|
-
super(message)
|
|
324
|
-
end
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
class ConfigurationError < StandardError; end
|
|
328
|
-
class ValidationError < StandardError; end
|
|
329
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KapsoClientRuby
|
|
4
|
+
module Errors
|
|
5
|
+
# Error categories mapped from the JavaScript implementation
|
|
6
|
+
ERROR_CATEGORIES = {
|
|
7
|
+
'authorization' => :authorization,
|
|
8
|
+
'permission' => :permission,
|
|
9
|
+
'parameter' => :parameter,
|
|
10
|
+
'throttling' => :throttling,
|
|
11
|
+
'template' => :template,
|
|
12
|
+
'media' => :media,
|
|
13
|
+
'phone_registration' => :phone_registration,
|
|
14
|
+
'integrity' => :integrity,
|
|
15
|
+
'business_eligibility' => :business_eligibility,
|
|
16
|
+
'reengagement_window' => :reengagement_window,
|
|
17
|
+
'waba_config' => :waba_config,
|
|
18
|
+
'flow' => :flow,
|
|
19
|
+
'synchronization' => :synchronization,
|
|
20
|
+
'server' => :server,
|
|
21
|
+
'unknown' => :unknown
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# Error codes and their categories
|
|
25
|
+
ERROR_CODE_CATEGORIES = {
|
|
26
|
+
0 => :authorization,
|
|
27
|
+
190 => :authorization,
|
|
28
|
+
3 => :permission,
|
|
29
|
+
10 => :permission,
|
|
30
|
+
(200..219) => :permission,
|
|
31
|
+
4 => :throttling,
|
|
32
|
+
80007 => :throttling,
|
|
33
|
+
130429 => :throttling,
|
|
34
|
+
131048 => :throttling,
|
|
35
|
+
131056 => :throttling,
|
|
36
|
+
33 => :parameter,
|
|
37
|
+
100 => :parameter,
|
|
38
|
+
130472 => :parameter,
|
|
39
|
+
131008 => :parameter,
|
|
40
|
+
131009 => :parameter,
|
|
41
|
+
131021 => :parameter,
|
|
42
|
+
131026 => :parameter,
|
|
43
|
+
131051 => :media,
|
|
44
|
+
131052 => :media,
|
|
45
|
+
131053 => :media,
|
|
46
|
+
131000 => :server,
|
|
47
|
+
131016 => :server,
|
|
48
|
+
131057 => :server,
|
|
49
|
+
133004 => :server,
|
|
50
|
+
133005 => :server,
|
|
51
|
+
368 => :integrity,
|
|
52
|
+
130497 => :integrity,
|
|
53
|
+
131031 => :integrity,
|
|
54
|
+
131047 => :reengagement_window,
|
|
55
|
+
131037 => :waba_config,
|
|
56
|
+
131042 => :business_eligibility,
|
|
57
|
+
131045 => :phone_registration,
|
|
58
|
+
133000 => :phone_registration,
|
|
59
|
+
133006 => :phone_registration,
|
|
60
|
+
133008 => :phone_registration,
|
|
61
|
+
133009 => :phone_registration,
|
|
62
|
+
133010 => :phone_registration,
|
|
63
|
+
133015 => :phone_registration,
|
|
64
|
+
133016 => :phone_registration,
|
|
65
|
+
132000 => :template,
|
|
66
|
+
132001 => :template,
|
|
67
|
+
132005 => :template,
|
|
68
|
+
132007 => :template,
|
|
69
|
+
132012 => :template,
|
|
70
|
+
132015 => :template,
|
|
71
|
+
132016 => :template,
|
|
72
|
+
132068 => :flow,
|
|
73
|
+
132069 => :flow,
|
|
74
|
+
134011 => :business_eligibility,
|
|
75
|
+
135000 => :parameter,
|
|
76
|
+
2593107 => :synchronization,
|
|
77
|
+
2593108 => :synchronization
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
80
|
+
# Error codes that should not be retried
|
|
81
|
+
DO_NOT_RETRY_CODES = [131049, 131050, 131047, 368, 130497, 131031].freeze
|
|
82
|
+
|
|
83
|
+
# Error codes that require token refresh
|
|
84
|
+
REFRESH_TOKEN_CODES = [0, 190].freeze
|
|
85
|
+
|
|
86
|
+
class GraphApiError < StandardError
|
|
87
|
+
attr_reader :http_status, :code, :type, :details, :error_subcode,
|
|
88
|
+
:fbtrace_id, :error_data, :category, :retry_hint, :raw_response, :retry_after
|
|
89
|
+
|
|
90
|
+
def initialize(message: nil, http_status:, code: nil, type: nil, details: nil,
|
|
91
|
+
error_subcode: nil, fbtrace_id: nil, error_data: nil,
|
|
92
|
+
category: nil, retry_hint: nil, raw_response: nil, retry_after: nil)
|
|
93
|
+
@http_status = http_status
|
|
94
|
+
@code = code || http_status
|
|
95
|
+
@type = type || 'GraphApiError'
|
|
96
|
+
@details = details
|
|
97
|
+
@error_subcode = error_subcode
|
|
98
|
+
@fbtrace_id = fbtrace_id
|
|
99
|
+
@error_data = error_data
|
|
100
|
+
@retry_after = retry_after
|
|
101
|
+
@category = category || categorize_error_code(@code, @http_status)
|
|
102
|
+
@retry_hint = retry_hint || derive_retry_hint
|
|
103
|
+
@raw_response = raw_response
|
|
104
|
+
|
|
105
|
+
error_message = message || build_error_message
|
|
106
|
+
super(error_message)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class << self
|
|
110
|
+
def from_response(response, body = nil, raw_text = nil)
|
|
111
|
+
http_status = response.status
|
|
112
|
+
retry_after_ms = parse_retry_after(response.headers['retry-after'])
|
|
113
|
+
|
|
114
|
+
# Ensure body is a hash for processing
|
|
115
|
+
unless body.is_a?(Hash)
|
|
116
|
+
if body.is_a?(String)
|
|
117
|
+
begin
|
|
118
|
+
body = JSON.parse(body)
|
|
119
|
+
rescue JSON::ParserError
|
|
120
|
+
body = {}
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
body = {}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Check for Graph API error envelope
|
|
128
|
+
if body.key?('error')
|
|
129
|
+
error_payload = body['error']
|
|
130
|
+
code = error_payload['code'] || http_status
|
|
131
|
+
type = error_payload['type'] || 'GraphApiError'
|
|
132
|
+
details = error_payload.is_a?(Hash) ? error_payload.dig('error_data', 'details') : nil
|
|
133
|
+
|
|
134
|
+
new(
|
|
135
|
+
message: error_payload['message'],
|
|
136
|
+
http_status: http_status,
|
|
137
|
+
code: code,
|
|
138
|
+
type: type,
|
|
139
|
+
details: details,
|
|
140
|
+
error_subcode: error_payload['error_subcode'],
|
|
141
|
+
fbtrace_id: error_payload['fbtrace_id'],
|
|
142
|
+
error_data: error_payload['error_data'],
|
|
143
|
+
retry_hint: build_retry_hint_with_delay(code, http_status, retry_after_ms),
|
|
144
|
+
raw_response: body
|
|
145
|
+
)
|
|
146
|
+
elsif body.is_a?(Hash) && body.key?('error') && body['error'].is_a?(String)
|
|
147
|
+
# Kapso proxy error format
|
|
148
|
+
category = http_status >= 500 ? :server : categorize_error_code(nil, http_status)
|
|
149
|
+
new(
|
|
150
|
+
message: body['error'],
|
|
151
|
+
http_status: http_status,
|
|
152
|
+
code: http_status,
|
|
153
|
+
category: category,
|
|
154
|
+
retry_hint: build_retry_hint_with_delay(http_status, http_status, retry_after_ms),
|
|
155
|
+
raw_response: body
|
|
156
|
+
)
|
|
157
|
+
else
|
|
158
|
+
# Generic HTTP error
|
|
159
|
+
category = http_status >= 500 ? :server : categorize_error_code(nil, http_status)
|
|
160
|
+
message = build_default_message(http_status, nil, raw_text)
|
|
161
|
+
|
|
162
|
+
new(
|
|
163
|
+
message: message,
|
|
164
|
+
http_status: http_status,
|
|
165
|
+
code: http_status,
|
|
166
|
+
category: category,
|
|
167
|
+
retry_hint: build_retry_hint_with_delay(http_status, http_status, retry_after_ms),
|
|
168
|
+
raw_response: raw_text || body
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def parse_retry_after(header)
|
|
176
|
+
return nil unless header
|
|
177
|
+
|
|
178
|
+
# Try parsing as number of seconds
|
|
179
|
+
if header.match?(/^\d+$/)
|
|
180
|
+
header.to_i * 1000
|
|
181
|
+
else
|
|
182
|
+
# Try parsing as HTTP date
|
|
183
|
+
begin
|
|
184
|
+
date = Time.parse(header)
|
|
185
|
+
diff = (date.to_f - Time.now.to_f) * 1000
|
|
186
|
+
diff > 0 ? diff.to_i : 0
|
|
187
|
+
rescue ArgumentError
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def categorize_error_code(code, http_status)
|
|
194
|
+
return :authorization if http_status == 401
|
|
195
|
+
return :permission if http_status == 403
|
|
196
|
+
return :parameter if http_status == 404
|
|
197
|
+
return :throttling if http_status == 429
|
|
198
|
+
return :server if http_status >= 500
|
|
199
|
+
return :parameter if http_status >= 400 && http_status < 500
|
|
200
|
+
|
|
201
|
+
if code
|
|
202
|
+
ERROR_CODE_CATEGORIES.each do |key, category|
|
|
203
|
+
if key.is_a?(Range)
|
|
204
|
+
return category if key.include?(code)
|
|
205
|
+
elsif key == code
|
|
206
|
+
return category
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Check permission range
|
|
211
|
+
return :permission if code >= 200 && code <= 299
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
:unknown
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def build_retry_hint_with_delay(code, http_status, retry_after_ms)
|
|
218
|
+
if retry_after_ms
|
|
219
|
+
{ action: :retry_after, retry_after_ms: retry_after_ms }
|
|
220
|
+
elsif DO_NOT_RETRY_CODES.include?(code)
|
|
221
|
+
{ action: :do_not_retry }
|
|
222
|
+
elsif REFRESH_TOKEN_CODES.include?(code)
|
|
223
|
+
{ action: :refresh_token }
|
|
224
|
+
elsif http_status >= 500
|
|
225
|
+
{ action: :retry }
|
|
226
|
+
else
|
|
227
|
+
{ action: :fix_and_retry }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def build_default_message(status, details = nil, raw_text = nil)
|
|
232
|
+
if details
|
|
233
|
+
"Meta API request failed with status #{status}: #{details}"
|
|
234
|
+
elsif raw_text && !raw_text.strip.empty?
|
|
235
|
+
"Meta API request failed with status #{status}: #{raw_text}"
|
|
236
|
+
else
|
|
237
|
+
"Meta API request failed with status #{status}"
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def auth_error?
|
|
243
|
+
category == :authorization
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def rate_limit?
|
|
247
|
+
category == :throttling
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def temporary?
|
|
251
|
+
[:throttling, :server, :synchronization].include?(category) ||
|
|
252
|
+
http_status >= 500 ||
|
|
253
|
+
[1, 2, 17, 341].include?(code)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def template_error?
|
|
257
|
+
category == :template
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def requires_token_refresh?
|
|
261
|
+
category == :authorization || REFRESH_TOKEN_CODES.include?(code)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def retryable?
|
|
265
|
+
![:do_not_retry].include?(retry_hint[:action])
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def to_h
|
|
269
|
+
{
|
|
270
|
+
name: self.class.name,
|
|
271
|
+
message: message,
|
|
272
|
+
http_status: http_status,
|
|
273
|
+
code: code,
|
|
274
|
+
type: type,
|
|
275
|
+
details: details,
|
|
276
|
+
error_subcode: error_subcode,
|
|
277
|
+
fbtrace_id: fbtrace_id,
|
|
278
|
+
category: category,
|
|
279
|
+
retry_hint: retry_hint,
|
|
280
|
+
raw_response: raw_response
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
private
|
|
285
|
+
|
|
286
|
+
def categorize_error_code(code, http_status)
|
|
287
|
+
self.class.send(:categorize_error_code, code, http_status)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def derive_retry_hint
|
|
291
|
+
if DO_NOT_RETRY_CODES.include?(code)
|
|
292
|
+
{ action: :do_not_retry }
|
|
293
|
+
elsif REFRESH_TOKEN_CODES.include?(code)
|
|
294
|
+
{ action: :refresh_token }
|
|
295
|
+
elsif http_status >= 500
|
|
296
|
+
{ action: :retry }
|
|
297
|
+
else
|
|
298
|
+
{ action: :fix_and_retry }
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def build_error_message
|
|
303
|
+
if details
|
|
304
|
+
"Meta API request failed with status #{http_status}: #{details}"
|
|
305
|
+
elsif raw_response.is_a?(String) && !raw_response.strip.empty?
|
|
306
|
+
"Meta API request failed with status #{http_status}: #{raw_response}"
|
|
307
|
+
else
|
|
308
|
+
"Meta API request failed with status #{http_status}"
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
class KapsoProxyRequiredError < StandardError
|
|
314
|
+
attr_reader :feature, :help_url
|
|
315
|
+
|
|
316
|
+
def initialize(feature)
|
|
317
|
+
@feature = feature
|
|
318
|
+
@help_url = 'https://kapso.ai/'
|
|
319
|
+
|
|
320
|
+
message = "#{feature} is only available via the Kapso Proxy. " \
|
|
321
|
+
"Set base_url to https://app.kapso.ai/api/meta and provide kapso_api_key. " \
|
|
322
|
+
"Create a free account at #{help_url}"
|
|
323
|
+
super(message)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
class ConfigurationError < StandardError; end
|
|
328
|
+
class ValidationError < StandardError; end
|
|
329
|
+
|
|
330
|
+
# Flow-specific errors
|
|
331
|
+
class FlowDecryptionError < StandardError
|
|
332
|
+
attr_reader :original_error
|
|
333
|
+
|
|
334
|
+
def initialize(message, original_error = nil)
|
|
335
|
+
@original_error = original_error
|
|
336
|
+
super(message)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
class FlowEncryptionError < StandardError
|
|
341
|
+
attr_reader :original_error
|
|
342
|
+
|
|
343
|
+
def initialize(message, original_error = nil)
|
|
344
|
+
@original_error = original_error
|
|
345
|
+
super(message)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
330
349
|
end
|