Skip to content

Commit 51e07ff

Browse files
committed
Fix double render in Slack::PuzzlesController when notification fails
When send_message rescued a Slack::Web::Api::Errors::SlackError and called head :unprocessable_entity, execution returned to create which then called render json:, raising AbstractController::DoubleRenderError. Added return if performed? to halt after send_message when it has already rendered.
1 parent dd297ba commit 51e07ff

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

app/controllers/slack/puzzles_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def create
1313
view = SlackClient::Views::Success.new.create
1414
notification_message = SlackClient::Messages::NewPuzzleNotification.new(puzzle).create
1515
send_message(notification_message, channel_id: ENV.fetch("SLACK_NOTIFICATIONS_CHANNEL", nil))
16+
return if performed?
1617
else
1718
view = SlackClient::Views::Failure.new.create
1819
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
require "test_helper"
2+
3+
class Slack::PuzzlesControllerTest < ActionDispatch::IntegrationTest
4+
setup do
5+
ENV["SLACK_SIGNING_SECRET"] = "test_signing_secret"
6+
end
7+
8+
test "renders ok when puzzle is saved and notification succeeds" do
9+
# Use a payload that fails validation so send_message is never called,
10+
# verifying the action renders 200 without a double render.
11+
params = { payload: puzzle_payload(question: "") }
12+
13+
post slack_puzzle_path, params: params,
14+
headers: slack_headers(body: params.to_query)
15+
16+
assert_response :ok
17+
end
18+
19+
test "does not double render when Slack notification fails after puzzle is saved" do
20+
# Override send_message to simulate a Slack API error that calls head :unprocessable_entity.
21+
# Without the fix, this causes AbstractController::DoubleRenderError because create
22+
# still calls render json: after send_message returns.
23+
original = Slack::ApplicationController.instance_method(:send_message)
24+
Slack::ApplicationController.define_method(:send_message) { |*| head :unprocessable_entity }
25+
26+
params = { payload: puzzle_payload(question: "What is unique about Ruby's blocks?") }
27+
28+
post slack_puzzle_path, params: params,
29+
headers: slack_headers(body: params.to_query)
30+
31+
assert_response :unprocessable_entity
32+
ensure
33+
Slack::ApplicationController.define_method(:send_message, original)
34+
end
35+
36+
private
37+
38+
def puzzle_payload(question: "What is Ruby?")
39+
{
40+
user: { id: "U123" },
41+
view: {
42+
state: {
43+
values: {
44+
question: { question: { value: question } },
45+
answer: { answer: { selected_option: { value: "ruby" } } },
46+
explanation: { explanation: { value: "It is a programming language." } },
47+
link: { link: { value: nil } }
48+
}
49+
}
50+
}
51+
}.to_json
52+
end
53+
54+
def slack_headers(secret: ENV["SLACK_SIGNING_SECRET"], timestamp: Time.now.to_i, body: "")
55+
ts = timestamp.to_s
56+
sig = "v0=" + OpenSSL::HMAC.hexdigest("SHA256", secret, "v0:#{ts}:#{body}")
57+
{ "X-Slack-Request-Timestamp" => ts, "X-Slack-Signature" => sig }
58+
end
59+
end

0 commit comments

Comments
 (0)