Skip to content

Commit 2bf472d

Browse files
add rescue method to handle errors at StreamDecorator
1 parent d867965 commit 2bf472d

File tree

3 files changed

+120
-6
lines changed

3 files changed

+120
-6
lines changed

react_on_rails_pro/lib/react_on_rails_pro/stream_request.rb

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def initialize(component)
1010
# @param position [Symbol] The position of the chunk in the stream (:first, :middle, or :last)
1111
# The position parameter is used by actions that add content to the beginning or end of the stream
1212
@actions = [] # List to store all actions
13+
@rescue_blocks = []
1314
end
1415

1516
# Add a prepend action
@@ -39,27 +40,45 @@ def append
3940
self # Return self to allow chaining
4041
end
4142

43+
def rescue(&block)
44+
@rescue_blocks << block
45+
self # Return self to allow chaining
46+
end
47+
4248
def handle_chunk(chunk, position)
4349
@actions.reduce(chunk) do |acc, action|
4450
action.call(acc, position)
4551
end
4652
end
4753

48-
def each_chunk
49-
return enum_for(:each_chunk) unless block_given?
54+
def each_chunk(&block)
55+
return enum_for(:each_chunk) unless block
5056

5157
first_chunk = true
5258
@component.each_chunk do |chunk|
5359
position = first_chunk ? :first : :middle
5460
modified_chunk = handle_chunk(chunk, position)
55-
yield modified_chunk
61+
block.call(modified_chunk)
5662
first_chunk = false
5763
end
5864

5965
# The last chunk contains the append content after the transformation
6066
# All transformations are applied to the append content
6167
last_chunk = handle_chunk("", :last)
62-
yield last_chunk unless last_chunk.empty?
68+
block.call(last_chunk) unless last_chunk.empty?
69+
rescue StandardError => err
70+
current_error = err
71+
rescue_block_index = 0
72+
while current_error.present? && (rescue_block_index < @rescue_blocks.size)
73+
begin
74+
@rescue_blocks[rescue_block_index].call(current_error, &block)
75+
current_error = nil
76+
rescue StandardError => inner_error
77+
current_error = inner_error
78+
end
79+
rescue_block_index += 1
80+
end
81+
raise current_error if current_error.present?
6382
end
6483
end
6584

@@ -86,6 +105,9 @@ def each_chunk(&block)
86105
break
87106
rescue HTTPX::HTTPError => e
88107
send_bundle = handle_http_error(e, error_body, send_bundle)
108+
rescue HTTPX::ReadTimeoutError => e
109+
raise ReactOnRailsPro::Error, "Time out error while server side render streaming a component.\n" \
110+
"Original error:\n#{e}\n#{e.backtrace}"
89111
end
90112
end
91113

@@ -132,6 +154,7 @@ def loop_response_lines(response)
132154
line = "".b
133155

134156
response.each do |chunk|
157+
response.instance_variable_set(:@react_on_rails_received_first_chunk, true)
135158
line << chunk
136159

137160
while (idx = line.index("\n"))

react_on_rails_pro/spec/react_on_rails_pro/stream_decorator_spec.rb

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,93 @@
6262
expect(chunks.last).to end_with("-end")
6363
end
6464
end
65+
66+
describe "#rescue" do
67+
it "catches the error happens inside the component" do
68+
allow(mock_component).to receive(:each_chunk).and_raise(StandardError.new "Fake Error")
69+
mocked_block = mock_block
70+
71+
stream_decorator.rescue(&mocked_block.block)
72+
chunks = []
73+
expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
74+
75+
expect(mocked_block).to have_received(:call) do |error|
76+
expect(error).to be_a(StandardError)
77+
expect(error.message).to eq("Fake Error")
78+
end
79+
expect(chunks).to eq([])
80+
end
81+
82+
it "catches the error happens inside subsequent component calls" do
83+
allow(mock_component).to receive(:each_chunk).and_yield("Chunk1").and_raise(ArgumentError.new "Fake Error")
84+
mocked_block = mock_block
85+
86+
stream_decorator.rescue(&mocked_block.block)
87+
chunks = []
88+
expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
89+
90+
expect(mocked_block).to have_received(:call) do |error|
91+
expect(chunks).to eq(["Chunk1"])
92+
expect(error).to be_a(ArgumentError)
93+
expect(error.message).to eq("Fake Error")
94+
end
95+
expect(chunks).to eq(["Chunk1"])
96+
end
97+
98+
it "can yield values to the stream" do
99+
allow(mock_component).to receive(:each_chunk).and_yield("Chunk1").and_raise(ArgumentError.new "Fake Error")
100+
mocked_block = mock_block
101+
102+
stream_decorator.rescue(&mocked_block.block)
103+
chunks = []
104+
expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
105+
106+
expect(mocked_block).to have_received(:call) do |error, &inner_block|
107+
expect(chunks).to eq(["Chunk1"])
108+
expect(error).to be_a(ArgumentError)
109+
expect(error.message).to eq("Fake Error")
110+
111+
inner_block.call "Chunk from rescue block"
112+
inner_block.call "Chunk2 from rescue block"
113+
end
114+
expect(chunks).to eq(["Chunk1", "Chunk from rescue block", "Chunk2 from rescue block"])
115+
end
116+
117+
it "can convert the error into another error" do
118+
allow(mock_component).to receive(:each_chunk).and_raise(StandardError.new "Fake Error")
119+
mocked_block = mock_block do |error|
120+
expect(error).to be_a(StandardError)
121+
expect(error.message).to eq("Fake Error")
122+
raise ArgumentError.new "Another Error"
123+
end
124+
125+
stream_decorator.rescue(&mocked_block.block)
126+
chunks = []
127+
expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.to raise_error(ArgumentError, "Another Error")
128+
expect(chunks).to eq([])
129+
end
130+
131+
it "chains multiple rescue blocks" do
132+
allow(mock_component).to receive(:each_chunk).and_yield("Chunk1").and_raise(StandardError.new "Fake Error")
133+
fist_rescue_block = mock_block do |error, &block|
134+
expect(error).to be_a(StandardError)
135+
expect(error.message).to eq("Fake Error")
136+
block.call "Chunk from first rescue block"
137+
raise ArgumentError.new "Another Error"
138+
end
139+
140+
second_rescue_block = mock_block do |error, &block|
141+
expect(error).to be_a(ArgumentError)
142+
expect(error.message).to eq("Another Error")
143+
block.call "Chunk from second rescue block"
144+
end
145+
146+
stream_decorator.rescue(&fist_rescue_block.block)
147+
stream_decorator.rescue(&second_rescue_block.block)
148+
chunks = []
149+
expect { stream_decorator.each_chunk { |chunk| chunks << chunk } }.not_to raise_error
150+
151+
expect(chunks).to eq(["Chunk1", "Chunk from first rescue block", "Chunk from second rescue block"])
152+
end
153+
end
65154
end

react_on_rails_pro/spec/react_on_rails_pro/support/mock_block_helper.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ module MockBlockHelper
99
# mocked_block = mock_block
1010
# testing_method_taking_block(&mocked_block.block)
1111
# expect(mocked_block).to have_received(:call).with(1, 2, 3)
12-
def mock_block(return_value: nil)
12+
def mock_block(&block)
1313
double("BlockMock").tap do |mock| # rubocop:disable RSpec/VerifiedDoubles
14-
allow(mock).to receive(:call) { return_value }
14+
allow(mock).to receive(:call) do |*args, &inner_block|
15+
block.call(*args, &inner_block) if block
16+
end
1517
def mock.block
1618
method(:call).to_proc
1719
end

0 commit comments

Comments
 (0)