Skip to content

Commit f474d52

Browse files
CopilotGrantBirki
andcommitted
Implement error! method for handler subclasses
Co-authored-by: GrantBirki <[email protected]>
1 parent 296ed19 commit f474d52

File tree

6 files changed

+156
-20
lines changed

6 files changed

+156
-20
lines changed

lib/hooks/app/api.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require_relative "auth/auth"
88
require_relative "rack_env_builder"
99
require_relative "../plugins/handlers/base"
10+
require_relative "../plugins/handlers/error"
1011
require_relative "../plugins/handlers/default"
1112
require_relative "../core/logger_factory"
1213
require_relative "../core/log"
@@ -110,6 +111,25 @@ def self.create(config:, endpoints:, log:)
110111
status 200
111112
content_type "application/json"
112113
response.to_json
114+
rescue Hooks::Plugins::Handlers::Error => e
115+
# Handler called error! method - return the specified error response
116+
log.info("handler returned error response: #{handler_class_name} - status: #{e.status} - body: #{e.body}")
117+
118+
# Call lifecycle hooks: on_response (treating error! as a valid response)
119+
if defined?(rack_env)
120+
Core::PluginLoader.lifecycle_plugins.each do |plugin|
121+
plugin.on_response(rack_env, e.body)
122+
end
123+
end
124+
125+
status e.status
126+
content_type "application/json"
127+
case e.body
128+
when String
129+
e.body
130+
else
131+
e.body.to_json
132+
end
113133
rescue StandardError => e
114134
err_msg = "Error processing webhook event with handler: #{handler_class_name} - #{e.message} " \
115135
"- request_id: #{request_id} - path: #{full_path} - method: #{http_method} - " \

lib/hooks/plugins/handlers/base.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative "../../core/global_components"
44
require_relative "../../core/component_access"
5+
require_relative "error"
56

67
module Hooks
78
module Plugins
@@ -23,6 +24,28 @@ class Base
2324
def call(payload:, headers:, env:, config:)
2425
raise NotImplementedError, "Handler must implement #call method"
2526
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
2649
end
2750
end
2851
end

lib/hooks/plugins/handlers/error.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

spec/acceptance/acceptance_tests.rb

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -496,26 +496,15 @@ def expired_unix_timestamp(seconds_ago = 600)
496496
end
497497

498498
it "sends a POST request to the /webhooks/boomtown_with_error endpoint and it explodes" do
499-
# TODO: Fix this acceptance test - the current error looks like this:
500-
# 1) Hooks endpoints boomtown_with_error sends a POST request to the /webhooks/boomtown_with_error endpoint and it explodes
501-
# Failure/Error: expect(response.body).to include(expected_body_content) if expected_body_content
502-
#expected "{\"error\":\"server_error\",\"message\":\"undefined method 'error!' for an instance of BoomtownWithE...thread_pool.rb:167:in 'block in #Puma::ThreadPool#spawn_thread'\",\"handler\":\"BoomtownWithError\"}" to include "the payload triggered a boomtown error"
503-
# ./spec/acceptance/acceptance_tests.rb:28:in 'RSpec::ExampleGroups::Hooks#expect_response'
504-
# ./spec/acceptance/acceptance_tests.rb:501:in 'block (4 levels) in <top (required)>'
505-
506-
# payload = { boom: true }.to_json
507-
# response = make_request(:post, "/webhooks/boomtown_with_error", payload, json_headers)
508-
# expect_response(response, Net::HTTPInternalServerError, "the payload triggered a boomtown error")
509-
510-
# body = parse_json_response(response)
511-
# expect(body["error"]).to eq("server_error")
512-
# expect(body["message"]).to eq("the payload triggered a boomtown error")
513-
# expect(body).to have_key("backtrace")
514-
# expect(body["backtrace"]).to be_a(String)
515-
# expect(body).to have_key("request_id")
516-
# expect(body["request_id"]).to be_a(String)
517-
# expect(body).to have_key("handler")
518-
# expect(body["handler"]).to eq("BoomtownWithError")
499+
payload = { boom: true }.to_json
500+
response = make_request(:post, "/webhooks/boomtown_with_error", payload, json_headers)
501+
expect_response(response, Net::HTTPInternalServerError, "the payload triggered a boomtown error")
502+
503+
body = parse_json_response(response)
504+
expect(body["error"]).to eq("boomtown_with_error")
505+
expect(body["message"]).to eq("the payload triggered a boomtown error")
506+
expect(body).to have_key("request_id")
507+
expect(body["request_id"]).to be_a(String)
519508
end
520509
end
521510
end

spec/unit/lib/hooks/handlers/base_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,42 @@ def report(error_or_message, context = {})
250250
end
251251
end
252252
end
253+
254+
describe "#error!" do
255+
let(:handler) { described_class.new }
256+
257+
it "raises a handler error with default status 500" do
258+
expect {
259+
handler.error!("Something went wrong")
260+
}.to raise_error(Hooks::Plugins::Handlers::Error) do |error|
261+
expect(error.body).to eq("Something went wrong")
262+
expect(error.status).to eq(500)
263+
end
264+
end
265+
266+
it "raises a handler error with custom status" do
267+
expect {
268+
handler.error!({ error: "validation_failed", message: "Invalid input" }, 400)
269+
}.to raise_error(Hooks::Plugins::Handlers::Error) do |error|
270+
expect(error.body).to eq({ error: "validation_failed", message: "Invalid input" })
271+
expect(error.status).to eq(400)
272+
end
273+
end
274+
275+
it "can be called from subclasses" do
276+
test_handler = Class.new(described_class) do
277+
def call(payload:, headers:, env:, config:)
278+
error!("Custom error from subclass", 422)
279+
end
280+
end
281+
282+
handler = test_handler.new
283+
expect {
284+
handler.call(payload: {}, headers: {}, env: {}, config: {})
285+
}.to raise_error(Hooks::Plugins::Handlers::Error) do |error|
286+
expect(error.body).to eq("Custom error from subclass")
287+
expect(error.status).to eq(422)
288+
end
289+
end
290+
end
253291
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
describe Hooks::Plugins::Handlers::Error do
4+
describe "#initialize" do
5+
it "creates an error with default status 500" do
6+
error = described_class.new("test error")
7+
expect(error.body).to eq("test error")
8+
expect(error.status).to eq(500)
9+
expect(error.message).to eq("Handler error: 500 - test error")
10+
end
11+
12+
it "creates an error with custom status" do
13+
error = described_class.new({ error: "validation_failed" }, 400)
14+
expect(error.body).to eq({ error: "validation_failed" })
15+
expect(error.status).to eq(400)
16+
expect(error.message).to eq("Handler error: 400 - {error: \"validation_failed\"}")
17+
end
18+
19+
it "converts status to integer" do
20+
error = described_class.new("test", "404")
21+
expect(error.status).to eq(404)
22+
end
23+
end
24+
25+
describe "inheritance" do
26+
it "inherits from StandardError" do
27+
expect(described_class.ancestors).to include(StandardError)
28+
end
29+
end
30+
end

0 commit comments

Comments
 (0)