From 4829dcd4fc28c83a1848b28dc9a310b78d6eaae2 Mon Sep 17 00:00:00 2001 From: Abanoub Ghadban Date: Mon, 26 Aug 2024 17:29:18 +0300 Subject: [PATCH] handle errors happen during streaming components --- lib/react_on_rails/helper.rb | 41 ++++++++++----- .../react_component/render_options.rb | 11 ++++ .../src/serverRenderReactComponent.ts | 50 +++++++++++++++---- 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 737021ee1..a439670ae 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -526,6 +526,22 @@ def props_string(props) props.is_a?(String) ? props : props.to_json end + def raise_prerender_error(json_result, react_component_name, props, js_code) + raise ReactOnRails::PrerenderError.new( + component_name: react_component_name, + props: sanitized_props_string(props), + err: nil, + js_code: js_code, + console_messages: json_result["consoleReplayScript"] + ) + end + + def should_raise_streaming_prerender_error?(chunk_json_result, render_options) + chunk_json_result["hasErrors"] && + ((render_options.raise_on_prerender_error && !chunk_json_result["isShellReady"]) || + (render_options.raise_non_shell_server_rendering_errors && chunk_json_result["isShellReady"])) + end + # Returns object with values that are NOT html_safe! def server_rendered_react_component(render_options) return { "html" => "", "consoleReplayScript" => "" } unless render_options.prerender @@ -573,19 +589,20 @@ def server_rendered_react_component(render_options) js_code: js_code) end - # TODO: handle errors for streams - return result if render_options.stream? - - if result["hasErrors"] && render_options.raise_on_prerender_error - # We caught this exception on our backtrace handler - raise ReactOnRails::PrerenderError.new(component_name: react_component_name, - # Sanitize as this might be browser logged - props: sanitized_props_string(props), - err: nil, - js_code: js_code, - console_messages: result["consoleReplayScript"]) - + if render_options.stream? + # It doesn't make any transformation, it just listening to the streamed chunks and raise error if it has errors + result.transform do |chunk_json_result| + if should_raise_streaming_prerender_error?(chunk_json_result, render_options) + raise_prerender_error(chunk_json_result, react_component_name, props, js_code) + end + chunk_json_result + end + else + if result["hasErrors"] && render_options.raise_on_prerender_error + raise_prerender_error(result, react_component_name, props, js_code) + end end + result end diff --git a/lib/react_on_rails/react_component/render_options.rb b/lib/react_on_rails/react_component/render_options.rb index 17b40bf2b..8a8a628ab 100644 --- a/lib/react_on_rails/react_component/render_options.rb +++ b/lib/react_on_rails/react_component/render_options.rb @@ -87,6 +87,10 @@ def raise_on_prerender_error retrieve_configuration_value_for(:raise_on_prerender_error) end + def raise_non_shell_server_rendering_errors + retrieve_react_on_rails_pro_config_value_for(:raise_non_shell_server_rendering_errors) + end + def logging_on_server retrieve_configuration_value_for(:logging_on_server) end @@ -124,6 +128,13 @@ def retrieve_configuration_value_for(key) ReactOnRails.configuration.public_send(key) end end + + def retrieve_react_on_rails_pro_config_value_for(key) + options.fetch(key) do + return nil unless ReactOnRails::Utils.react_on_rails_pro? + ReactOnRailsPro.configuration.public_send(key) + end + end end end end diff --git a/node_package/src/serverRenderReactComponent.ts b/node_package/src/serverRenderReactComponent.ts index fd40e1f47..7e5b6fcf2 100644 --- a/node_package/src/serverRenderReactComponent.ts +++ b/node_package/src/serverRenderReactComponent.ts @@ -169,8 +169,8 @@ const serverRenderReactComponent: typeof serverRenderReactComponentInternal = (o const stringToStream = (str: string): Readable => { const stream = new PassThrough(); - stream.push(str); - stream.push(null); + stream.write(str); + stream.end(); return stream; }; @@ -178,6 +178,8 @@ export const streamServerRenderedReactComponent = (options: RenderParams): Reada const { name, domNodeId, trace, props, railsContext, throwJsErrors } = options; let renderResult: null | Readable = null; + let hasErrors = false; + let isShellReady = false; let previouslyReplayedConsoleMessages: number = 0; try { @@ -200,24 +202,47 @@ See https://github.com/shakacode/react_on_rails#renderer-functions`); throw new Error('Server rendering of streams is not supported for server render hashes or promises.'); } - const transformStream = new Transform({ + const transformStream = new PassThrough({ transform(chunk, _, callback) { const htmlChunk = chunk.toString(); + console.log('htmlChunk', htmlChunk); const consoleReplayScript = buildConsoleReplay(previouslyReplayedConsoleMessages); previouslyReplayedConsoleMessages = console.history?.length || 0; const jsonChunk = JSON.stringify({ html: htmlChunk, consoleReplayScript, + hasErrors, + isShellReady, }); - + this.push(jsonChunk); callback(); } }); - ReactDOMServer.renderToPipeableStream(reactRenderingResult) - .pipe(transformStream); + const renderingStream = ReactDOMServer.renderToPipeableStream(reactRenderingResult, { + onShellError(error) { + // Can't through error here if throwJsErrors is true because the error will happen inside the stream + // And will not be handled by any catch clause + hasErrors = true; + transformStream.write(handleError({ + e: error as any, + name, + serverSide: true, + })); + transformStream.end(); + }, + onShellReady() { + isShellReady = true; + renderingStream.pipe(transformStream); + }, + onError(_) { + // Can't through error here if throwJsErrors is true because the error will happen inside the stream + // And will not be handled by any catch clause + hasErrors = true; + }, + }); renderResult = transformStream; } catch (e: any) { @@ -225,10 +250,15 @@ See https://github.com/shakacode/react_on_rails#renderer-functions`); throw e; } - renderResult = stringToStream(handleError({ - e, - name, - serverSide: true, + renderResult = stringToStream(JSON.stringify({ + html: handleError({ + e, + name, + serverSide: true, + }), + consoleReplayScript: buildConsoleReplay(previouslyReplayedConsoleMessages), + hasErrors: true, + isShellReady, })); }