action_webhook 1.0.0 → 1.2.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.
- checksums.yaml +4 -4
- data/lib/action_webhook/base.rb +90 -12
- data/lib/action_webhook/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5fd47bb2e72c2bd12feab3682031b00603f271279b435eeb56b5a4e7dba6bccb
|
4
|
+
data.tar.gz: a4e22834b739227926772b9b8d18f4dbc74c1d7f7fc06bcbc1174707a306897d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd8df71521ef095e6f52aad36daacb1e518ec400a0cb1aa8919568dbe240731097679f18933bd9d30406d17a02685ffd79570189067e84405c80968c834de015
|
7
|
+
data.tar.gz: f42e11c74f275f4206c992a9e96a6e3b37eb40ed4f53191dbbef2192c596a2f039c172bd9d888f33ae0c18b9dc504a25d40055f86fba847ba5188c4e578af672
|
data/lib/action_webhook/base.rb
CHANGED
@@ -4,6 +4,10 @@ module ActionWebhook
|
|
4
4
|
# Subclass this and define webhook methods (e.g. `created`, `updated`) that
|
5
5
|
# define instance variables and call deliver to send webhooks.
|
6
6
|
#
|
7
|
+
# Headers can be provided in two formats:
|
8
|
+
# 1. Hash format: { 'Authorization' => 'Bearer token', 'Content-Type' => 'application/json' }
|
9
|
+
# 2. Array format: [{ 'key' => 'Authorization', 'value' => 'Bearer token' }, { 'key' => 'Content-Type', 'value' => 'application/json' }]
|
10
|
+
#
|
7
11
|
# Example:
|
8
12
|
#
|
9
13
|
# class UserWebhook < ActionWebhook::Base
|
@@ -41,10 +45,26 @@ module ActionWebhook
|
|
41
45
|
#
|
42
46
|
# def created(user)
|
43
47
|
# @user = user
|
44
|
-
#
|
45
|
-
#
|
48
|
+
# # Headers can be provided as a hash
|
49
|
+
# endpoints_with_hash_headers = WebhookSubscription.where(event: 'user.created').map do |sub|
|
50
|
+
# {
|
51
|
+
# url: sub.url,
|
52
|
+
# headers: { 'Authorization' => "Bearer #{sub.token}", 'X-Custom-Header' => 'value' }
|
53
|
+
# }
|
46
54
|
# end
|
47
|
-
#
|
55
|
+
#
|
56
|
+
# # Or headers can be provided as an array of key/value objects (useful for database storage)
|
57
|
+
# endpoints_with_array_headers = WebhookSubscription.where(event: 'user.created').map do |sub|
|
58
|
+
# {
|
59
|
+
# url: sub.url,
|
60
|
+
# headers: [
|
61
|
+
# { 'key' => 'Authorization', 'value' => "Bearer #{sub.token}" },
|
62
|
+
# { 'key' => 'X-Custom-Header', 'value' => 'value' }
|
63
|
+
# ]
|
64
|
+
# }
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# deliver(endpoints_with_hash_headers)
|
48
68
|
# end
|
49
69
|
# end
|
50
70
|
#
|
@@ -81,12 +101,21 @@ module ActionWebhook
|
|
81
101
|
@attempts += 1
|
82
102
|
response = process_webhook
|
83
103
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
104
|
+
# Separate successful and failed responses
|
105
|
+
successful_responses = response.select { |r| r[:success] }
|
106
|
+
failed_responses = response.reject { |r| r[:success] }
|
107
|
+
|
108
|
+
# Invoke success callback for successful deliveries
|
109
|
+
invoke_callback(self.class.after_deliver_callback, successful_responses) if successful_responses.any?
|
110
|
+
|
111
|
+
# Handle failed responses
|
112
|
+
if failed_responses.any? && @attempts < self.class.max_retries
|
113
|
+
# Extract failed webhook details for retry
|
114
|
+
failed_webhook_details = failed_responses.map { |r| @webhook_details.find { |detail| detail[:url] == r[:url] } }.compact
|
115
|
+
retry_with_backoff(failed_webhook_details)
|
116
|
+
elsif failed_responses.any?
|
117
|
+
# All retries exhausted for failed URLs
|
118
|
+
invoke_callback(self.class.after_retries_exhausted_callback, failed_responses)
|
90
119
|
end
|
91
120
|
|
92
121
|
response
|
@@ -225,8 +254,51 @@ module ActionWebhook
|
|
225
254
|
assigns
|
226
255
|
end
|
227
256
|
|
257
|
+
# Builds HTTP headers for webhook requests
|
258
|
+
#
|
259
|
+
# Supports two input formats:
|
260
|
+
# 1. Hash format: { 'Authorization' => 'Bearer token', 'Content-Type' => 'application/json' }
|
261
|
+
# 2. Array format: [{ 'key' => 'Authorization', 'value' => 'Bearer token' }, { 'key' => 'Content-Type', 'value' => 'application/json' }]
|
262
|
+
#
|
263
|
+
# The array format is useful when storing headers in databases where you need
|
264
|
+
# structured data with separate key and value fields.
|
265
|
+
#
|
266
|
+
# @param detail_headers [Hash, Array, nil] Headers in hash or array format
|
267
|
+
# @return [Hash] Formatted headers hash ready for HTTP request
|
228
268
|
def build_headers(detail_headers)
|
229
|
-
|
269
|
+
# Handle both hash format and array format with key/value objects
|
270
|
+
processed_headers = case detail_headers
|
271
|
+
when Array
|
272
|
+
# Transform array of header hashes [{'key': 'value'}] into a single hash
|
273
|
+
detail_headers.each_with_object({}) do |header_item, acc|
|
274
|
+
next unless header_item.is_a?(Hash)
|
275
|
+
|
276
|
+
# Handle string keys
|
277
|
+
if header_item.key?('key') && header_item.key?('value')
|
278
|
+
key = header_item['key']
|
279
|
+
value = header_item['value']
|
280
|
+
acc[key.to_s] = value.to_s if key && value
|
281
|
+
# Handle symbol keys
|
282
|
+
elsif header_item.key?(:key) && header_item.key?(:value)
|
283
|
+
key = header_item[:key]
|
284
|
+
value = header_item[:value]
|
285
|
+
acc[key.to_s] = value.to_s if key && value
|
286
|
+
else
|
287
|
+
# Log warning for malformed header items
|
288
|
+
logger&.warn("Skipping malformed header item: #{header_item.inspect}")
|
289
|
+
end
|
290
|
+
end
|
291
|
+
when Hash
|
292
|
+
# Ensure all keys and values are strings for consistency
|
293
|
+
detail_headers.transform_keys(&:to_s).transform_values(&:to_s)
|
294
|
+
when NilClass
|
295
|
+
{}
|
296
|
+
else
|
297
|
+
logger&.warn("Unknown header format: #{detail_headers.class}. Expected Hash or Array.")
|
298
|
+
{}
|
299
|
+
end
|
300
|
+
|
301
|
+
headers = default_headers.merge(processed_headers)
|
230
302
|
headers["Content-Type"] = "application/json" unless headers.key?("Content-Type")
|
231
303
|
headers["X-Webhook-Attempt"] = @attempts.to_s if @attempts.positive?
|
232
304
|
headers
|
@@ -318,13 +390,19 @@ module ActionWebhook
|
|
318
390
|
end
|
319
391
|
end
|
320
392
|
|
321
|
-
def retry_with_backoff
|
393
|
+
def retry_with_backoff(failed_webhook_details = nil)
|
394
|
+
# Use failed webhook details if provided, otherwise retry all
|
395
|
+
retry_details = failed_webhook_details || @webhook_details
|
396
|
+
|
322
397
|
delay = calculate_backoff_delay
|
323
|
-
logger.info("Scheduling webhook retry #{@attempts + 1}/#{self.class.max_retries} in #{delay} seconds")
|
398
|
+
logger.info("Scheduling webhook retry #{@attempts + 1}/#{self.class.max_retries} for #{retry_details.size} URLs in #{delay} seconds")
|
324
399
|
|
325
400
|
job_class = resolve_job_class
|
326
401
|
serialized_webhook = serialize
|
327
402
|
|
403
|
+
# Update the webhook details to only include failed URLs
|
404
|
+
serialized_webhook["webhook_details"] = retry_details
|
405
|
+
|
328
406
|
enqueue_retry_job(job_class, serialized_webhook, delay)
|
329
407
|
end
|
330
408
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: action_webhook
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vinay Uttam Vemparala
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-07-30 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: httparty
|