Skip to content

Commit

Permalink
handle errors happen during streaming components
Browse files Browse the repository at this point in the history
  • Loading branch information
AbanoubGhadban committed Aug 26, 2024
1 parent bcfddd6 commit 4829dcd
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 22 deletions.
41 changes: 29 additions & 12 deletions lib/react_on_rails/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions lib/react_on_rails/react_component/render_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
50 changes: 40 additions & 10 deletions node_package/src/serverRenderReactComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,17 @@ 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;
};

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

let renderResult: null | Readable = null;
let hasErrors = false;
let isShellReady = false;
let previouslyReplayedConsoleMessages: number = 0;

try {
Expand All @@ -200,35 +202,63 @@ 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) {
if (throwJsErrors) {
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,
}));
}

Expand Down

0 comments on commit 4829dcd

Please sign in to comment.