Skip to content

Commit 43accf3

Browse files
Improved Error handling while generating RSC payload & don't include 'react-dom/server' in the RSC bundle (#1888)
- Moved all utility functions needed by `ReactOnRailsRSC` to separate files, so these files don't include any imports for `react-dom/server` module. - Improved Error handling while generating RSC payload by transferring the erorr to the rails side and logging the error and the stack
1 parent 4f7f773 commit 43accf3

19 files changed

+188
-40
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Changes since the last non-beta release.
2929

3030
- **Improved Error Messages**: Error messages for version mismatches and package configuration issues now include package-manager-specific installation commands (npm, yarn, pnpm, bun). [PR #1881](https://github.com/shakacode/react_on_rails/pull/1881) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
3131

32+
- **Improved RSC Payload Error Handling**: Errors that happen during generation of RSC payload are transferred properly to rails side and logs the error message and stack. [PR #1888](https://github.com/shakacode/react_on_rails/pull/1888) by [AbanoubGhadban](https://github.com/AbanoubGhadban).
33+
3234
#### Bug Fixes
3335

3436
- **Use as Git dependency**: All packages can now be installed as Git dependencies. This is useful for development and testing purposes. See [CONTRIBUTING.md](./CONTRIBUTING.md#git-dependencies) for documentation. [PR #1873](https://github.com/shakacode/react_on_rails/pull/1873) by [alexeyr-ci2](https://github.com/alexeyr-ci2).

knip.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const config: KnipConfig = {
4242

4343
// React on Rails core package workspace
4444
'packages/react-on-rails': {
45-
entry: ['src/ReactOnRails.full.ts!', 'src/ReactOnRails.client.ts!'],
45+
entry: ['src/ReactOnRails.full.ts!', 'src/ReactOnRails.client.ts!', 'src/base/full.rsc.ts!'],
4646
project: ['src/**/*.[jt]s{x,}!', 'tests/**/*.[jt]s{x,}', '!lib/**'],
4747
ignore: [
4848
// Jest setup and test utilities - not detected by Jest plugin in workspace setup

packages/react-on-rails-pro/jest.config.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ export default {
2828

2929
// Allow Jest to transform react-on-rails package from node_modules
3030
transformIgnorePatterns: ['node_modules/(?!react-on-rails)'],
31-
31+
// RSC tests needs the node condition "react-server" to run
32+
// So, before running these tests, we set "NODE_CONDITIONS=react-server"
33+
testEnvironmentOptions: process.env.NODE_CONDITIONS
34+
? {
35+
customExportConditions: process.env.NODE_CONDITIONS.split(','),
36+
}
37+
: {},
3238
// Set root directory to current package
3339
rootDir: '.',
3440
};

packages/react-on-rails-pro/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"build": "yarn run clean && yarn run tsc",
88
"build-watch": "yarn run clean && yarn run tsc --watch",
99
"clean": "rm -rf ./lib",
10-
"test": "jest tests",
10+
"test": "yarn test:non-rsc && yarn test:rsc",
11+
"test:non-rsc": "jest tests --testPathIgnorePatterns=\".*(.rsc.test.).*\"",
12+
"test:rsc": "NODE_CONDITIONS=react-server jest tests/*.rsc.test.*",
1113
"type-check": "yarn run tsc --noEmit --noErrorTruncation",
1214
"prepack": "nps build.prepack",
1315
"prepare": "nps build.prepack",

packages/react-on-rails-pro/src/RSCRoute.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,15 @@ export type RSCRouteProps = {
7777

7878
const PromiseWrapper = ({ promise }: { promise: Promise<React.ReactNode> }) => {
7979
// React.use is available in React 18.3+
80-
return React.use(promise);
80+
const promiseResult = React.use(promise);
81+
82+
// In case that an error happened during the rendering of the RSC payload before the rendering of the component itself starts
83+
// RSC bundle will return an error object serialized inside the RSC payload
84+
if (promiseResult instanceof Error) {
85+
throw promiseResult;
86+
}
87+
88+
return promiseResult;
8189
};
8290

8391
const RSCRoute = ({ componentName, componentProps }: RSCRouteProps): React.ReactNode => {

packages/react-on-rails-pro/src/ReactOnRailsRSC.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ import {
2222
StreamRenderState,
2323
StreamableComponentResult,
2424
} from 'react-on-rails/types';
25-
import handleError from 'react-on-rails/handleError';
2625
import { convertToError } from 'react-on-rails/serverRenderUtils';
26+
import handleError from './handleErrorRSC.ts';
2727
import ReactOnRails from './ReactOnRails.full.ts';
2828

2929
import {
@@ -51,7 +51,7 @@ const streamRenderRSCComponent = (
5151
isShellReady: true,
5252
};
5353

54-
const { pipeToTransform, readableStream, emitError, writeChunk, endStream } =
54+
const { pipeToTransform, readableStream, emitError } =
5555
transformRenderStreamChunksToResultObject(renderState);
5656

5757
const reportError = (error: Error) => {
@@ -87,8 +87,7 @@ const streamRenderRSCComponent = (
8787
const error = convertToError(e);
8888
reportError(error);
8989
const errorHtml = handleError({ e: error, name: options.name, serverSide: true });
90-
writeChunk(errorHtml);
91-
endStream();
90+
pipeToTransform(errorHtml);
9291
});
9392

9493
readableStream.on('end', () => {
@@ -99,7 +98,7 @@ const streamRenderRSCComponent = (
9998

10099
ReactOnRails.serverRenderRSCReactComponent = (options: RSCRenderParams) => {
101100
try {
102-
return streamServerRenderedComponent(options, streamRenderRSCComponent);
101+
return streamServerRenderedComponent(options, streamRenderRSCComponent, handleError);
103102
} finally {
104103
console.history = [];
105104
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Readable } from 'stream';
2+
import { ErrorOptions } from 'react-on-rails/types';
3+
import handleErrorAsString from 'react-on-rails/handleError';
4+
5+
const handleError = (options: ErrorOptions) => {
6+
const htmlString = handleErrorAsString(options);
7+
return Readable.from([htmlString]);
8+
};
9+
10+
export default handleError;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ErrorOptions } from 'react-on-rails/types';
2+
import { renderToPipeableStream } from 'react-on-rails-rsc/server.node';
3+
import generateRenderingErrorMessage from 'react-on-rails/generateRenderingErrorMessage';
4+
5+
const handleError = (options: ErrorOptions) => {
6+
const msg = generateRenderingErrorMessage(options);
7+
return renderToPipeableStream(new Error(msg), {
8+
filePathToModuleMetadata: {},
9+
moduleLoading: { prefix: '', crossOrigin: null },
10+
});
11+
};
12+
13+
export default handleError;

packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import { Readable } from 'stream';
1616

17-
import handleError from 'react-on-rails/handleError';
1817
import { renderToPipeableStream } from 'react-on-rails/ReactDOMServer';
1918
import { convertToError } from 'react-on-rails/serverRenderUtils';
2019
import {
@@ -24,11 +23,12 @@ import {
2423
StreamableComponentResult,
2524
} from 'react-on-rails/types';
2625
import injectRSCPayload from './injectRSCPayload.ts';
27-
import {
26+
import {
27+
streamServerRenderedComponent,
2828
StreamingTrackers,
2929
transformRenderStreamChunksToResultObject,
30-
streamServerRenderedComponent,
31-
} from './streamingUtils.ts';
30+
} from './streamingUtils.ts';
31+
import handleError from './handleError.ts';
3232

3333
const streamRenderReactComponent = (
3434
reactRenderingResult: StreamableComponentResult,
@@ -55,9 +55,8 @@ const streamRenderReactComponent = (
5555
};
5656

5757
const sendErrorHtml = (error: Error) => {
58-
const errorHtml = handleError({ e: error, name: componentName, serverSide: true });
59-
writeChunk(errorHtml);
60-
endStream();
58+
const errorHtmlStream = handleError({ e: error, name: componentName, serverSide: true });
59+
pipeToTransform(errorHtmlStream);
6160
};
6261

6362
assertRailsContextWithServerStreamingCapabilities(railsContext);
@@ -102,6 +101,6 @@ const streamRenderReactComponent = (
102101
};
103102

104103
const streamServerRenderedReactComponent = (options: RenderParams): Readable =>
105-
streamServerRenderedComponent(options, streamRenderReactComponent);
104+
streamServerRenderedComponent(options, streamRenderReactComponent, handleError);
106105

107106
export default streamServerRenderedReactComponent;

packages/react-on-rails-pro/src/streamingUtils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { PassThrough, Readable } from 'stream';
1818
import createReactOutput from 'react-on-rails/createReactOutput';
1919
import { isPromise, isServerRenderHash } from 'react-on-rails/isServerRenderResult';
2020
import buildConsoleReplay from 'react-on-rails/buildConsoleReplay';
21-
import handleError from 'react-on-rails/handleError';
2221
import { createResultObject, convertToError, validateComponent } from 'react-on-rails/serverRenderUtils';
2322
import {
2423
RenderParams,
@@ -27,6 +26,7 @@ import {
2726
PipeableOrReadableStream,
2827
RailsContextWithServerStreamingCapabilities,
2928
assertRailsContextWithServerComponentMetadata,
29+
ErrorOptions,
3030
} from 'react-on-rails/types';
3131
import * as ComponentRegistry from './ComponentRegistry.ts';
3232
import PostSSRHookTracker from './PostSSRHookTracker.ts';
@@ -179,6 +179,7 @@ type StreamRenderer<T, P extends RenderParams> = (
179179
export const streamServerRenderedComponent = <T, P extends RenderParams>(
180180
options: P,
181181
renderStrategy: StreamRenderer<T, P>,
182+
handleError: (options: ErrorOptions) => PipeableOrReadableStream,
182183
): T => {
183184
const { name: componentName, domNodeId, trace, props, railsContext, throwJsErrors } = options;
184185

@@ -233,7 +234,7 @@ export const streamServerRenderedComponent = <T, P extends RenderParams>(
233234

234235
return renderStrategy(reactRenderingResult, optionsWithStreamingCapabilities, streamingTrackers);
235236
} catch (e) {
236-
const { readableStream, writeChunk, emitError, endStream } = transformRenderStreamChunksToResultObject({
237+
const { readableStream, pipeToTransform, emitError } = transformRenderStreamChunksToResultObject({
237238
hasErrors: true,
238239
isShellReady: false,
239240
result: null,
@@ -243,9 +244,8 @@ export const streamServerRenderedComponent = <T, P extends RenderParams>(
243244
}
244245

245246
const error = convertToError(e);
246-
const htmlResult = handleError({ e: error, name: componentName, serverSide: true });
247-
writeChunk(htmlResult);
248-
endStream();
247+
const htmlResultStream = handleError({ e: error, name: componentName, serverSide: true });
248+
pipeToTransform(htmlResultStream);
249249
return readableStream as T;
250250
}
251251
};

0 commit comments

Comments
 (0)