hooks-ruby 0.1.0 → 0.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/README.md +3 -3
- data/lib/hooks/app/api.rb +48 -31
- data/lib/hooks/app/auth/auth.rb +21 -6
- data/lib/hooks/app/endpoints/catchall.rb +35 -17
- data/lib/hooks/app/helpers.rb +12 -9
- data/lib/hooks/app/rack_env_builder.rb +85 -0
- data/lib/hooks/plugins/handlers/base.rb +25 -1
- data/lib/hooks/plugins/handlers/default.rb +7 -1
- data/lib/hooks/plugins/handlers/error.rb +36 -0
- data/lib/hooks/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae1757617b84df588de7eb343799b93d1dfc6326c3d54ab1a2d578aa2d3e3b8e
|
4
|
+
data.tar.gz: a39cfa8162346d536ab5a543dfcf7517162282ba3a74098cd276bc76c636a41c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: da5353fda88b8d348e7b743e155642376ad886b5e77a38cb08148a9a6fbb5bece88c099125bb4738d41a09e3d3e0b5c29bd317bafa3789caa595d02f1c7217a2
|
7
|
+
data.tar.gz: 0ba9c613e80a889da7b1fd7b6b4b84b53190aae6db27184d7caa0a90806c3d266f0fff06c4bff1cef702c0879f772056fde5c1b697d817852130167eb0724ebd
|
data/README.md
CHANGED
@@ -58,7 +58,7 @@ Here is a very high-level overview of how Hooks works:
|
|
58
58
|
```ruby
|
59
59
|
# file: plugins/handlers/my_custom_handler.rb
|
60
60
|
class MyCustomHandler < Hooks::Plugins::Handlers::Base
|
61
|
-
def call(payload:, headers:, config:)
|
61
|
+
def call(payload:, headers:, env:, config:)
|
62
62
|
# Process the incoming webhook - optionally use the payload and headers
|
63
63
|
# to perform some action or validation
|
64
64
|
# For this example, we will just return a success message
|
@@ -233,7 +233,7 @@ Create custom handler plugins in the `plugins/handlers` directory to process inc
|
|
233
233
|
```ruby
|
234
234
|
# file: plugins/handlers/hello_handler.rb
|
235
235
|
class HelloHandler < Hooks::Plugins::Handlers::Base
|
236
|
-
def call(payload:, headers:, config:)
|
236
|
+
def call(payload:, headers:, env:, config:)
|
237
237
|
# Process the incoming webhook - optionally use the payload and headers
|
238
238
|
# to perform some action or validation
|
239
239
|
# For this example, we will just return a success message
|
@@ -251,7 +251,7 @@ And another handler plugin for the `/goodbye` endpoint:
|
|
251
251
|
```ruby
|
252
252
|
# file: plugins/handlers/goodbye_handler.rb
|
253
253
|
class GoodbyeHandler < Hooks::Plugins::Handlers::Base
|
254
|
-
def call(payload:, headers:, config:)
|
254
|
+
def call(payload:, headers:, env:, config:)
|
255
255
|
# Ditto for the goodbye endpoint
|
256
256
|
{
|
257
257
|
message: "goodbye webhook processed successfully",
|
data/lib/hooks/app/api.rb
CHANGED
@@ -5,7 +5,9 @@ require "json"
|
|
5
5
|
require "securerandom"
|
6
6
|
require_relative "helpers"
|
7
7
|
require_relative "auth/auth"
|
8
|
+
require_relative "rack_env_builder"
|
8
9
|
require_relative "../plugins/handlers/base"
|
10
|
+
require_relative "../plugins/handlers/error"
|
9
11
|
require_relative "../plugins/handlers/default"
|
10
12
|
require_relative "../core/logger_factory"
|
11
13
|
require_relative "../core/log"
|
@@ -65,41 +67,27 @@ module Hooks
|
|
65
67
|
Core::LogContext.with(request_context) do
|
66
68
|
begin
|
67
69
|
# Build Rack environment for lifecycle hooks
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
"CONTENT_LENGTH" => request.content_length,
|
78
|
-
"REMOTE_ADDR" => request.env["REMOTE_ADDR"],
|
79
|
-
"hooks.request_id" => request_id,
|
80
|
-
"hooks.handler" => handler_class_name,
|
81
|
-
"hooks.endpoint_config" => endpoint_config,
|
82
|
-
"hooks.start_time" => start_time.iso8601,
|
83
|
-
"hooks.full_path" => full_path
|
84
|
-
}
|
85
|
-
|
86
|
-
# Add HTTP headers to environment
|
87
|
-
headers.each do |key, value|
|
88
|
-
env_key = "HTTP_#{key.upcase.tr('-', '_')}"
|
89
|
-
rack_env[env_key] = value
|
90
|
-
end
|
70
|
+
rack_env_builder = RackEnvBuilder.new(
|
71
|
+
request,
|
72
|
+
headers,
|
73
|
+
request_context,
|
74
|
+
endpoint_config,
|
75
|
+
start_time,
|
76
|
+
full_path
|
77
|
+
)
|
78
|
+
rack_env = rack_env_builder.build
|
91
79
|
|
92
80
|
# Call lifecycle hooks: on_request
|
93
81
|
Core::PluginLoader.lifecycle_plugins.each do |plugin|
|
94
82
|
plugin.on_request(rack_env)
|
95
83
|
end
|
96
84
|
|
97
|
-
enforce_request_limits(config)
|
85
|
+
enforce_request_limits(config, request_context)
|
98
86
|
request.body.rewind
|
99
87
|
raw_body = request.body.read
|
100
88
|
|
101
89
|
if endpoint_config[:auth]
|
102
|
-
validate_auth!(raw_body, headers, endpoint_config, config)
|
90
|
+
validate_auth!(raw_body, headers, endpoint_config, config, request_context)
|
103
91
|
end
|
104
92
|
|
105
93
|
payload = parse_payload(raw_body, headers, symbolize: false)
|
@@ -109,6 +97,7 @@ module Hooks
|
|
109
97
|
response = handler.call(
|
110
98
|
payload:,
|
111
99
|
headers: processed_headers,
|
100
|
+
env: rack_env,
|
112
101
|
config: endpoint_config
|
113
102
|
)
|
114
103
|
|
@@ -122,22 +111,50 @@ module Hooks
|
|
122
111
|
status 200
|
123
112
|
content_type "application/json"
|
124
113
|
response.to_json
|
114
|
+
rescue Hooks::Plugins::Handlers::Error => e
|
115
|
+
# Handler called error! method - immediately return error response and exit the request
|
116
|
+
log.debug("handler #{handler_class_name} called `error!` method")
|
117
|
+
|
118
|
+
error_response = nil
|
119
|
+
|
120
|
+
status e.status
|
121
|
+
case e.body
|
122
|
+
when String
|
123
|
+
content_type "text/plain"
|
124
|
+
error_response = e.body
|
125
|
+
else
|
126
|
+
content_type "application/json"
|
127
|
+
error_response = e.body.to_json
|
128
|
+
end
|
129
|
+
|
130
|
+
return error_response
|
125
131
|
rescue StandardError => e
|
126
|
-
|
132
|
+
err_msg = "Error processing webhook event with handler: #{handler_class_name} - #{e.message} " \
|
133
|
+
"- request_id: #{request_id} - path: #{full_path} - method: #{http_method} - " \
|
134
|
+
"backtrace: #{e.backtrace.join("\n")}"
|
135
|
+
log.error(err_msg)
|
136
|
+
|
137
|
+
# call lifecycle hooks: on_error if the rack_env is available
|
138
|
+
# if the rack_env is not available, it means the error occurred before we could build it
|
127
139
|
if defined?(rack_env)
|
128
140
|
Core::PluginLoader.lifecycle_plugins.each do |plugin|
|
129
141
|
plugin.on_error(e, rack_env)
|
130
142
|
end
|
131
143
|
end
|
132
144
|
|
133
|
-
|
145
|
+
# construct a standardized error response
|
134
146
|
error_response = {
|
135
|
-
error:
|
136
|
-
|
147
|
+
error: "server_error",
|
148
|
+
message: "an unexpected error occurred while processing the request",
|
137
149
|
request_id:
|
138
150
|
}
|
139
|
-
|
140
|
-
|
151
|
+
|
152
|
+
# enrich the error response with details if not in production
|
153
|
+
error_response[:backtrace] = e.backtrace.join("\n") unless config[:production]
|
154
|
+
error_response[:message] = e.message unless config[:production]
|
155
|
+
error_response[:handler] = handler_class_name unless config[:production]
|
156
|
+
|
157
|
+
status determine_error_code(e)
|
141
158
|
content_type "application/json"
|
142
159
|
error_response.to_json
|
143
160
|
end
|
data/lib/hooks/app/auth/auth.rb
CHANGED
@@ -16,30 +16,45 @@ module Hooks
|
|
16
16
|
# @param headers [Hash] The request headers.
|
17
17
|
# @param endpoint_config [Hash] The endpoint configuration, must include :auth key.
|
18
18
|
# @param global_config [Hash] The global configuration (optional, for compatibility).
|
19
|
+
# @param request_context [Hash] Context for the request, e.g. request ID, path, handler (optional).
|
19
20
|
# @raise [StandardError] Raises error if authentication fails or is misconfigured.
|
20
21
|
# @return [void]
|
21
22
|
# @note This method will halt execution with an error if authentication fails.
|
22
|
-
def validate_auth!(payload, headers, endpoint_config, global_config = {})
|
23
|
+
def validate_auth!(payload, headers, endpoint_config, global_config = {}, request_context = {})
|
23
24
|
auth_config = endpoint_config[:auth]
|
25
|
+
request_id = request_context&.dig(:request_id)
|
24
26
|
|
25
27
|
# Ensure auth type is present and valid
|
26
28
|
auth_type = auth_config&.dig(:type)
|
27
29
|
unless auth_type&.is_a?(String) && !auth_type.strip.empty?
|
28
|
-
error
|
30
|
+
log.error("authentication configuration missing or invalid - request_id: #{request_id}")
|
31
|
+
error!({
|
32
|
+
error: "authentication_configuration_error",
|
33
|
+
message: "authentication configuration missing or invalid",
|
34
|
+
request_id:
|
35
|
+
}, 500)
|
29
36
|
end
|
30
37
|
|
31
38
|
# Get auth plugin from loaded plugins registry (boot-time loaded only)
|
32
39
|
begin
|
33
40
|
auth_class = Core::PluginLoader.get_auth_plugin(auth_type)
|
34
41
|
rescue => e
|
35
|
-
log.error("failed to load auth plugin '#{auth_type}': #{e.message}")
|
36
|
-
error!(
|
42
|
+
log.error("failed to load auth plugin '#{auth_type}': #{e.message} - request_id: #{request_id}")
|
43
|
+
error!({
|
44
|
+
error: "authentication_plugin_error",
|
45
|
+
message: "unsupported auth type '#{auth_type}'",
|
46
|
+
request_id:
|
47
|
+
}, 400)
|
37
48
|
end
|
38
49
|
|
39
50
|
log.debug("validating auth for request with auth_class: #{auth_class.name}")
|
40
51
|
unless auth_class.valid?(payload:, headers:, config: endpoint_config)
|
41
|
-
log.warn("authentication failed for request with auth_class: #{auth_class.name}")
|
42
|
-
error!(
|
52
|
+
log.warn("authentication failed for request with auth_class: #{auth_class.name} - request_id: #{request_id}")
|
53
|
+
error!({
|
54
|
+
error: "authentication_failed",
|
55
|
+
message: "authentication failed",
|
56
|
+
request_id:
|
57
|
+
}, 401)
|
43
58
|
end
|
44
59
|
end
|
45
60
|
|
@@ -27,20 +27,36 @@ module Hooks
|
|
27
27
|
# :nocov:
|
28
28
|
proc do
|
29
29
|
request_id = uuid
|
30
|
+
start_time = Time.now
|
30
31
|
|
31
32
|
# Use captured values
|
32
33
|
config = captured_config
|
33
34
|
log = captured_logger
|
34
35
|
|
36
|
+
full_path = "#{config[:root_path]}/#{params[:path]}"
|
37
|
+
|
38
|
+
handler_class_name = "DefaultHandler"
|
39
|
+
http_method = "post"
|
40
|
+
|
35
41
|
# Set request context for logging
|
36
42
|
request_context = {
|
37
43
|
request_id:,
|
38
|
-
path:
|
39
|
-
handler:
|
44
|
+
path: full_path,
|
45
|
+
handler: handler_class_name
|
40
46
|
}
|
41
47
|
|
42
48
|
Hooks::Core::LogContext.with(request_context) do
|
43
49
|
begin
|
50
|
+
rack_env_builder = RackEnvBuilder.new(
|
51
|
+
request,
|
52
|
+
headers,
|
53
|
+
request_context,
|
54
|
+
config,
|
55
|
+
start_time,
|
56
|
+
full_path
|
57
|
+
)
|
58
|
+
rack_env = rack_env_builder.build
|
59
|
+
|
44
60
|
# Enforce request limits
|
45
61
|
enforce_request_limits(config)
|
46
62
|
|
@@ -58,32 +74,34 @@ module Hooks
|
|
58
74
|
response = handler.call(
|
59
75
|
payload:,
|
60
76
|
headers:,
|
77
|
+
env: rack_env,
|
61
78
|
config: {}
|
62
79
|
)
|
63
80
|
|
64
|
-
log.info
|
65
|
-
|
66
|
-
# Return response as JSON string when using txt format
|
81
|
+
log.info("successfully processed webhook event with handler: #{handler_class_name}")
|
82
|
+
log.debug("processing duration: #{Time.now - start_time}s")
|
67
83
|
status 200
|
68
84
|
content_type "application/json"
|
69
|
-
|
70
|
-
|
85
|
+
response.to_json
|
71
86
|
rescue StandardError => e
|
72
|
-
|
87
|
+
err_msg = "Error processing webhook event with handler: #{handler_class_name} - #{e.message} " \
|
88
|
+
"- request_id: #{request_id} - path: #{full_path} - method: #{http_method} - " \
|
89
|
+
"backtrace: #{e.backtrace.join("\n")}"
|
90
|
+
log.error(err_msg)
|
73
91
|
|
74
|
-
#
|
92
|
+
# construct a standardized error response
|
75
93
|
error_response = {
|
76
|
-
error:
|
77
|
-
|
78
|
-
request_id:
|
94
|
+
error: "server_error",
|
95
|
+
message: "an unexpected error occurred while processing the request",
|
96
|
+
request_id:
|
79
97
|
}
|
80
98
|
|
81
|
-
#
|
82
|
-
unless config[:production]
|
83
|
-
|
84
|
-
|
99
|
+
# enrich the error response with details if not in production
|
100
|
+
error_response[:backtrace] = e.backtrace.join("\n") unless config[:production]
|
101
|
+
error_response[:message] = e.message unless config[:production]
|
102
|
+
error_response[:handler] = handler_class_name unless config[:production]
|
85
103
|
|
86
|
-
status
|
104
|
+
status determine_error_code(e)
|
87
105
|
content_type "application/json"
|
88
106
|
error_response.to_json
|
89
107
|
end
|
data/lib/hooks/app/helpers.rb
CHANGED
@@ -17,10 +17,11 @@ module Hooks
|
|
17
17
|
# Enforce request size and timeout limits
|
18
18
|
#
|
19
19
|
# @param config [Hash] The configuration hash, must include :request_limit
|
20
|
+
# @param request_context [Hash] Context for the request, e.g. request ID (optional)
|
20
21
|
# @raise [StandardError] Halts with error if request body is too large
|
21
22
|
# @return [void]
|
22
23
|
# @note Timeout enforcement should be handled at the server level (e.g., Puma)
|
23
|
-
def enforce_request_limits(config)
|
24
|
+
def enforce_request_limits(config, request_context = {})
|
24
25
|
# Optimized content length check - check most common sources first
|
25
26
|
content_length = request.content_length if respond_to?(:request) && request.respond_to?(:content_length)
|
26
27
|
|
@@ -34,7 +35,8 @@ module Hooks
|
|
34
35
|
content_length = content_length&.to_i
|
35
36
|
|
36
37
|
if content_length && content_length > config[:request_limit]
|
37
|
-
|
38
|
+
request_id = request_context&.dig(:request_id)
|
39
|
+
error!({ error: "request_body_too_large", message: "request body too large", request_id: }, 413)
|
38
40
|
end
|
39
41
|
|
40
42
|
# Note: Timeout enforcement would typically be handled at the server level (Puma, etc.)
|
@@ -76,13 +78,14 @@ module Hooks
|
|
76
78
|
# @return [Object] An instance of the loaded handler class
|
77
79
|
# @raise [StandardError] If handler cannot be found
|
78
80
|
def load_handler(handler_class_name)
|
79
|
-
# Get handler class from loaded plugins registry (boot
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
81
|
+
# Get handler class from loaded plugins registry (the registry is populated at boot time)
|
82
|
+
# NOTE: We create a new instance per request (not reuse boot-time instances) because:
|
83
|
+
# - Security: Prevents state pollution and information leakage between requests
|
84
|
+
# - Thread Safety: Avoids race conditions from shared instance state
|
85
|
+
# - Performance: Handler instantiation is fast; reusing instances provides minimal gain
|
86
|
+
# - Memory: Allows garbage collection of short-lived objects (Ruby GC optimization)
|
87
|
+
handler_class = Core::PluginLoader.get_handler_plugin(handler_class_name)
|
88
|
+
return handler_class.new
|
86
89
|
end
|
87
90
|
|
88
91
|
private
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hooks
|
4
|
+
module App
|
5
|
+
# Builds Rack environment hash for lifecycle hooks and handler processing
|
6
|
+
#
|
7
|
+
# This class centralizes the construction of the Rack environment that gets
|
8
|
+
# passed to lifecycle hooks and handlers, ensuring consistency and making
|
9
|
+
# it easy to reference the environment structure.
|
10
|
+
#
|
11
|
+
# @example Building a Rack environment
|
12
|
+
# builder = RackEnvBuilder.new(request, headers, request_context)
|
13
|
+
# rack_env = builder.build
|
14
|
+
#
|
15
|
+
class RackEnvBuilder
|
16
|
+
# Initialize the builder with required components
|
17
|
+
#
|
18
|
+
# @param request [Grape::Request] The Grape request object
|
19
|
+
# @param headers [Hash] Request headers hash
|
20
|
+
# @param request_context [Hash] Request context containing metadata
|
21
|
+
# @option request_context [String] :request_id Unique request identifier
|
22
|
+
# @option request_context [String] :handler Handler class name
|
23
|
+
# @param endpoint_config [Hash] Endpoint configuration
|
24
|
+
# @param start_time [Time] Request start time
|
25
|
+
# @param full_path [String] Full request path including root path
|
26
|
+
def initialize(request, headers, request_context, endpoint_config, start_time, full_path)
|
27
|
+
@request = request
|
28
|
+
@headers = headers
|
29
|
+
@request_context = request_context
|
30
|
+
@endpoint_config = endpoint_config
|
31
|
+
@start_time = start_time
|
32
|
+
@full_path = full_path
|
33
|
+
end
|
34
|
+
|
35
|
+
# Build the Rack environment hash
|
36
|
+
#
|
37
|
+
# Constructs a hash containing standard Rack environment variables
|
38
|
+
# plus Hooks-specific extensions for lifecycle hooks and handlers.
|
39
|
+
#
|
40
|
+
# @return [Hash] Complete Rack environment hash
|
41
|
+
def build
|
42
|
+
rack_env = build_base_environment
|
43
|
+
add_http_headers(rack_env)
|
44
|
+
rack_env
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Build the base Rack environment with standard and Hooks-specific variables
|
50
|
+
# This pretty much creates everything plus the kitchen sink. It will be very rich in information
|
51
|
+
# and will be used by lifecycle hooks and handlers to access request metadata.
|
52
|
+
#
|
53
|
+
# @return [Hash] Base environment hash
|
54
|
+
def build_base_environment
|
55
|
+
{
|
56
|
+
"REQUEST_METHOD" => @request.request_method,
|
57
|
+
"PATH_INFO" => @request.path_info,
|
58
|
+
"QUERY_STRING" => @request.query_string,
|
59
|
+
"HTTP_VERSION" => @request.env["HTTP_VERSION"],
|
60
|
+
"REQUEST_URI" => @request.url,
|
61
|
+
"SERVER_NAME" => @request.env["SERVER_NAME"],
|
62
|
+
"SERVER_PORT" => @request.env["SERVER_PORT"],
|
63
|
+
"CONTENT_TYPE" => @request.content_type,
|
64
|
+
"CONTENT_LENGTH" => @request.content_length,
|
65
|
+
"REMOTE_ADDR" => @request.env["REMOTE_ADDR"],
|
66
|
+
"hooks.request_id" => @request_context[:request_id],
|
67
|
+
"hooks.handler" => @request_context[:handler],
|
68
|
+
"hooks.endpoint_config" => @endpoint_config,
|
69
|
+
"hooks.start_time" => @start_time.iso8601,
|
70
|
+
"hooks.full_path" => @full_path
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
# Add HTTP headers to the environment with proper Rack naming convention
|
75
|
+
#
|
76
|
+
# @param rack_env [Hash] Environment hash to modify
|
77
|
+
def add_http_headers(rack_env)
|
78
|
+
@headers.each do |key, value|
|
79
|
+
env_key = "HTTP_#{key.upcase.tr('-', '_')}"
|
80
|
+
rack_env[env_key] = value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative "../../core/global_components"
|
4
4
|
require_relative "../../core/component_access"
|
5
|
+
require_relative "error"
|
5
6
|
|
6
7
|
module Hooks
|
7
8
|
module Plugins
|
@@ -16,12 +17,35 @@ module Hooks
|
|
16
17
|
#
|
17
18
|
# @param payload [Hash, String] Parsed request body (JSON Hash) or raw string
|
18
19
|
# @param headers [Hash] HTTP headers (string keys, optionally normalized - default is normalized)
|
20
|
+
# @param env [Hash] Rack environment (contains the request context, headers, etc - very rich context)
|
19
21
|
# @param config [Hash] Merged endpoint configuration including opts section (symbolized keys)
|
20
22
|
# @return [Hash, String, nil] Response body (will be auto-converted to JSON)
|
21
23
|
# @raise [NotImplementedError] if not implemented by subclass
|
22
|
-
def call(payload:, headers:, config:)
|
24
|
+
def call(payload:, headers:, env:, config:)
|
23
25
|
raise NotImplementedError, "Handler must implement #call method"
|
24
26
|
end
|
27
|
+
|
28
|
+
# Terminate request processing with a custom error response
|
29
|
+
#
|
30
|
+
# This method provides the same interface as Grape's `error!` method,
|
31
|
+
# allowing handlers to immediately stop processing and return a specific
|
32
|
+
# error response to the client.
|
33
|
+
#
|
34
|
+
# @param body [Object] The error body/data to return to the client
|
35
|
+
# @param status [Integer] The HTTP status code to return (default: 500)
|
36
|
+
# @raise [Hooks::Plugins::Handlers::Error] Always raises to terminate processing
|
37
|
+
#
|
38
|
+
# @example Return a custom error with status 400
|
39
|
+
# error!({ error: "validation_failed", message: "Invalid payload" }, 400)
|
40
|
+
#
|
41
|
+
# @example Return a simple string error with status 401
|
42
|
+
# error!("Unauthorized", 401)
|
43
|
+
#
|
44
|
+
# @example Return an error with default 500 status
|
45
|
+
# error!({ error: "internal_error", message: "Something went wrong" })
|
46
|
+
def error!(body, status = 500)
|
47
|
+
raise Error.new(body, status)
|
48
|
+
end
|
25
49
|
end
|
26
50
|
end
|
27
51
|
end
|
@@ -20,6 +20,7 @@ class DefaultHandler < Hooks::Plugins::Handlers::Base
|
|
20
20
|
#
|
21
21
|
# @param payload [Hash, String] The webhook payload (parsed JSON or raw string)
|
22
22
|
# @param headers [Hash<String, String>] HTTP headers from the webhook request
|
23
|
+
# @param env [Hash] Rack environment (contains the request context, headers, config, etc - very rich context)
|
23
24
|
# @param config [Hash] Endpoint configuration containing handler options
|
24
25
|
# @return [Hash] Response indicating successful processing
|
25
26
|
# @option config [Hash] :opts Additional handler-specific configuration options
|
@@ -29,10 +30,11 @@ class DefaultHandler < Hooks::Plugins::Handlers::Base
|
|
29
30
|
# response = handler.call(
|
30
31
|
# payload: { "event" => "push" },
|
31
32
|
# headers: { "Content-Type" => "application/json" },
|
33
|
+
# env: { "REQUEST_METHOD" => "POST", "hooks.request_id" => "12345" },
|
32
34
|
# config: { opts: {} }
|
33
35
|
# )
|
34
36
|
# # => { message: "webhook processed successfully", handler: "DefaultHandler", timestamp: "..." }
|
35
|
-
def call(payload:, headers:, config:)
|
37
|
+
def call(payload:, headers:, env:, config:)
|
36
38
|
|
37
39
|
log.info("🔔 Default handler invoked for webhook 🔔")
|
38
40
|
|
@@ -41,6 +43,10 @@ class DefaultHandler < Hooks::Plugins::Handlers::Base
|
|
41
43
|
log.debug("received payload: #{payload.inspect}")
|
42
44
|
end
|
43
45
|
|
46
|
+
if env
|
47
|
+
log.debug("default handler got a request with the following request_id: #{env['hooks.request_id']}")
|
48
|
+
end
|
49
|
+
|
44
50
|
{
|
45
51
|
message: "webhook processed successfully",
|
46
52
|
handler: "DefaultHandler",
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hooks
|
4
|
+
module Plugins
|
5
|
+
module Handlers
|
6
|
+
# Custom exception class for handler errors
|
7
|
+
#
|
8
|
+
# This exception is used when handlers call the `error!` method to
|
9
|
+
# immediately terminate request processing and return a specific error response.
|
10
|
+
# It carries the error details back to the Grape API context where it can be
|
11
|
+
# properly formatted and returned to the client.
|
12
|
+
#
|
13
|
+
# @example Usage in handler
|
14
|
+
# error!({ error: "validation_failed", message: "Invalid payload" }, 400)
|
15
|
+
#
|
16
|
+
# @see Hooks::Plugins::Handlers::Base#error!
|
17
|
+
class Error < StandardError
|
18
|
+
# @return [Object] The error body/data to return to the client
|
19
|
+
attr_reader :body
|
20
|
+
|
21
|
+
# @return [Integer] The HTTP status code to return
|
22
|
+
attr_reader :status
|
23
|
+
|
24
|
+
# Initialize a new handler error
|
25
|
+
#
|
26
|
+
# @param body [Object] The error body/data to return to the client
|
27
|
+
# @param status [Integer] The HTTP status code to return (default: 500)
|
28
|
+
def initialize(body, status = 500)
|
29
|
+
@body = body
|
30
|
+
@status = status.to_i
|
31
|
+
super("Handler error: #{status} - #{body}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/hooks/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hooks-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- github
|
@@ -129,6 +129,7 @@ files:
|
|
129
129
|
- lib/hooks/app/endpoints/health.rb
|
130
130
|
- lib/hooks/app/endpoints/version.rb
|
131
131
|
- lib/hooks/app/helpers.rb
|
132
|
+
- lib/hooks/app/rack_env_builder.rb
|
132
133
|
- lib/hooks/core/builder.rb
|
133
134
|
- lib/hooks/core/component_access.rb
|
134
135
|
- lib/hooks/core/config_loader.rb
|
@@ -145,6 +146,7 @@ files:
|
|
145
146
|
- lib/hooks/plugins/auth/timestamp_validator.rb
|
146
147
|
- lib/hooks/plugins/handlers/base.rb
|
147
148
|
- lib/hooks/plugins/handlers/default.rb
|
149
|
+
- lib/hooks/plugins/handlers/error.rb
|
148
150
|
- lib/hooks/plugins/instruments/failbot.rb
|
149
151
|
- lib/hooks/plugins/instruments/failbot_base.rb
|
150
152
|
- lib/hooks/plugins/instruments/stats.rb
|