Skip to content

Commit 3e4380b

Browse files
authored
Ensure connection is closed when raw SSE stream raises (#248)
* Ensure connection is closed when raw SSE stream raises Add an `ensure` block to `start_raw_stream` so the connection is always closed if the user's Proc raises an exception without calling `close`. This matches the cleanup behavior already present in `start_formatted_stream`, which has `ensure connection.close`. Without this fix, a Proc that raises before closing leaves the Iodine connection open until socket timeout. * Close connection only on exception, not on normal completion Address review feedback: the ensure block breaks async patterns where the proc spawns background fibers and returns early (e.g. Datastar SDK). Changed to rescue so the connection is only closed when the proc raises, not when it completes normally. Updated tests: - Proc raises → connection closed - Proc returns without closing (async pattern) → connection stays open - Proc closes itself → no interference
1 parent 5013a08 commit 3e4380b

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

lib/rage/sse/application.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,8 @@ def start_formatted_stream(connection)
5454

5555
def start_raw_stream(connection)
5656
@stream.call(Rage::SSE::ConnectionProxy.new(connection))
57+
rescue => e
58+
connection.close if connection.open?
59+
raise e
5760
end
5861
end

spec/sse/application_spec.rb

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Rage::SSE::Application do
4+
let(:connection) { MockSSEConnection.new }
5+
6+
class MockSSEConnection
7+
attr_reader :messages
8+
9+
def initialize
10+
@messages = []
11+
@open = true
12+
end
13+
14+
def write(data)
15+
@messages << data
16+
end
17+
18+
def close
19+
@open = false
20+
end
21+
22+
def open?
23+
@open
24+
end
25+
end
26+
27+
describe "#start_raw_stream" do
28+
it "closes the connection when the proc raises an exception" do
29+
failing_proc = ->(conn) {
30+
conn.write("data: before error\n\n")
31+
raise "boom"
32+
}
33+
34+
app = described_class.new(failing_proc)
35+
36+
expect {
37+
app.send(:start_raw_stream, connection)
38+
}.to raise_error(RuntimeError, "boom")
39+
40+
expect(connection.open?).to be false
41+
end
42+
43+
it "does not close the connection on normal completion" do
44+
async_proc = ->(conn) {
45+
conn.write("data: started\n\n")
46+
# Proc returns without closing — a background fiber will close later
47+
}
48+
49+
app = described_class.new(async_proc)
50+
app.send(:start_raw_stream, connection)
51+
52+
expect(connection.open?).to be true
53+
end
54+
55+
it "does not interfere when the proc closes the connection itself" do
56+
well_behaved_proc = ->(conn) {
57+
conn.write("data: hello\n\n")
58+
conn.close
59+
}
60+
61+
app = described_class.new(well_behaved_proc)
62+
app.send(:start_raw_stream, connection)
63+
64+
expect(connection.open?).to be false
65+
expect(connection.messages).to eq(["data: hello\n\n"])
66+
end
67+
end
68+
end

0 commit comments

Comments
 (0)