Skip to content

Commit 50bf32f

Browse files
handle errors happen during streaming components
1 parent cffaed8 commit 50bf32f

File tree

3 files changed

+84
-23
lines changed

3 files changed

+84
-23
lines changed

lib/react_on_rails/helper.rb

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,22 @@ def props_string(props)
570570
props.is_a?(String) ? props : props.to_json
571571
end
572572

573+
def raise_prerender_error(json_result, react_component_name, props, js_code)
574+
raise ReactOnRails::PrerenderError.new(
575+
component_name: react_component_name,
576+
props: sanitized_props_string(props),
577+
err: nil,
578+
js_code: js_code,
579+
console_messages: json_result["consoleReplayScript"]
580+
)
581+
end
582+
583+
def should_raise_streaming_prerender_error?(chunk_json_result, render_options)
584+
chunk_json_result["hasErrors"] &&
585+
((render_options.raise_on_prerender_error && !chunk_json_result["isShellReady"]) ||
586+
(render_options.raise_non_shell_server_rendering_errors && chunk_json_result["isShellReady"]))
587+
end
588+
573589
# Returns object with values that are NOT html_safe!
574590
def server_rendered_react_component(render_options)
575591
return { "html" => "", "consoleReplayScript" => "" } unless render_options.prerender
@@ -617,19 +633,20 @@ def server_rendered_react_component(render_options)
617633
js_code: js_code)
618634
end
619635

620-
# TODO: handle errors for streams
621-
return result if render_options.stream?
622-
623-
if result["hasErrors"] && render_options.raise_on_prerender_error
624-
# We caught this exception on our backtrace handler
625-
raise ReactOnRails::PrerenderError.new(component_name: react_component_name,
626-
# Sanitize as this might be browser logged
627-
props: sanitized_props_string(props),
628-
err: nil,
629-
js_code: js_code,
630-
console_messages: result["consoleReplayScript"])
631-
636+
if render_options.stream?
637+
# It doesn't make any transformation, it just listening to the streamed chunks and raise error if it has errors
638+
result.transform do |chunk_json_result|
639+
if should_raise_streaming_prerender_error?(chunk_json_result, render_options)
640+
raise_prerender_error(chunk_json_result, react_component_name, props, js_code)
641+
end
642+
chunk_json_result
643+
end
644+
else
645+
if result["hasErrors"] && render_options.raise_on_prerender_error
646+
raise_prerender_error(result, react_component_name, props, js_code)
647+
end
632648
end
649+
633650
result
634651
end
635652

lib/react_on_rails/react_component/render_options.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ def raise_on_prerender_error
8787
retrieve_configuration_value_for(:raise_on_prerender_error)
8888
end
8989

90+
def raise_non_shell_server_rendering_errors
91+
retrieve_react_on_rails_pro_config_value_for(:raise_non_shell_server_rendering_errors)
92+
end
93+
9094
def logging_on_server
9195
retrieve_configuration_value_for(:logging_on_server)
9296
end
@@ -128,6 +132,13 @@ def retrieve_configuration_value_for(key)
128132
ReactOnRails.configuration.public_send(key)
129133
end
130134
end
135+
136+
def retrieve_react_on_rails_pro_config_value_for(key)
137+
options.fetch(key) do
138+
return nil unless ReactOnRails::Utils.react_on_rails_pro?
139+
ReactOnRailsPro.configuration.public_send(key)
140+
end
141+
end
131142
end
132143
end
133144
end

node_package/src/serverRenderReactComponent.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ReactDOMServer from 'react-dom/server';
2-
import { PassThrough, Readable, Transform } from 'stream';
2+
import { PassThrough, Readable } from 'stream';
33
import type { ReactElement } from 'react';
44

55
import ComponentRegistry from './ComponentRegistry';
@@ -195,16 +195,19 @@ const serverRenderReactComponent: typeof serverRenderReactComponentInternal = (o
195195

196196
const stringToStream = (str: string): Readable => {
197197
const stream = new PassThrough();
198-
stream.push(str);
199-
stream.push(null);
198+
stream.write(str);
199+
stream.end();
200200
return stream;
201201
};
202202

203203
export const streamServerRenderedReactComponent = (options: RenderParams): Readable => {
204204
const { name: componentName, domNodeId, trace, props, railsContext, throwJsErrors } = options;
205205

206206
let renderResult: null | Readable = null;
207+
let hasErrors = false;
208+
let isShellReady = false;
207209
let previouslyReplayedConsoleMessages: number = 0;
210+
let consoleHistory: typeof console['history'] | undefined;
208211

209212
try {
210213
const componentObj = ComponentRegistry.get(componentName);
@@ -222,24 +225,49 @@ export const streamServerRenderedReactComponent = (options: RenderParams): Reada
222225
throw new Error('Server rendering of streams is not supported for server render hashes or promises.');
223226
}
224227

225-
const consoleHistory = console.history;
226-
const transformStream = new Transform({
228+
consoleHistory = console.history;
229+
const transformStream = new PassThrough({
227230
transform(chunk, _, callback) {
228231
const htmlChunk = chunk.toString();
232+
console.log('htmlChunk', htmlChunk);
229233
const consoleReplayScript = buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages);
230234
previouslyReplayedConsoleMessages = consoleHistory?.length || 0;
231235

232236
const jsonChunk = JSON.stringify({
233237
html: htmlChunk,
234238
consoleReplayScript,
239+
hasErrors,
240+
isShellReady,
235241
});
236-
242+
237243
this.push(jsonChunk);
238244
callback();
239245
}
240246
});
241247

242-
ReactDOMServer.renderToPipeableStream(reactRenderingResult).pipe(transformStream);
248+
const renderingStream = ReactDOMServer.renderToPipeableStream(reactRenderingResult, {
249+
onShellError(e) {
250+
// Can't through error here if throwJsErrors is true because the error will happen inside the stream
251+
// And will not be handled by any catch clause
252+
hasErrors = true;
253+
const error = e instanceof Error ? e : new Error(String(e));
254+
transformStream.write(handleError({
255+
e: error,
256+
name: componentName,
257+
serverSide: true,
258+
}));
259+
transformStream.end();
260+
},
261+
onShellReady() {
262+
isShellReady = true;
263+
renderingStream.pipe(transformStream);
264+
},
265+
onError() {
266+
// Can't through error here if throwJsErrors is true because the error will happen inside the stream
267+
// And will not be handled by any catch clause
268+
hasErrors = true;
269+
},
270+
});
243271

244272
renderResult = transformStream;
245273
} catch (e) {
@@ -248,10 +276,15 @@ export const streamServerRenderedReactComponent = (options: RenderParams): Reada
248276
}
249277

250278
const error = e instanceof Error ? e : new Error(String(e));
251-
renderResult = stringToStream(handleError({
252-
e: error,
253-
name: componentName,
254-
serverSide: true,
279+
renderResult = stringToStream(JSON.stringify({
280+
html: handleError({
281+
e: error,
282+
name: componentName,
283+
serverSide: true,
284+
}),
285+
consoleReplayScript: buildConsoleReplay(consoleHistory, previouslyReplayedConsoleMessages),
286+
hasErrors: true,
287+
isShellReady,
255288
}));
256289
}
257290

0 commit comments

Comments
 (0)