diff --git a/packages/react-on-rails-pro/src/RSCRequestTracker.ts b/packages/react-on-rails-pro/src/RSCRequestTracker.ts index 36116767ea..5d90d594fe 100644 --- a/packages/react-on-rails-pro/src/RSCRequestTracker.ts +++ b/packages/react-on-rails-pro/src/RSCRequestTracker.ts @@ -17,26 +17,10 @@ import { RSCPayloadStreamInfo, RSCPayloadCallback, RailsContextWithServerComponentMetadata, + GenerateRSCPayloadFunction, } from 'react-on-rails/types'; import { extractErrorMessage } from './utils.ts'; -/** - * Global function provided by React on Rails Pro for generating RSC payloads. - * - * This function is injected into the global scope during server-side rendering - * by the RORP rendering request. It handles the actual generation of React Server - * Component payloads on the server side. - * - * @see https://github.com/shakacode/react_on_rails_pro/blob/master/lib/react_on_rails_pro/server_rendering_js_code.rb - */ -declare global { - function generateRSCPayload( - componentName: string, - props: unknown, - railsContext: RailsContextWithServerComponentMetadata, - ): Promise; -} - /** * RSC Request Tracker - manages RSC payload generation and tracking for a single request. * @@ -52,8 +36,14 @@ class RSCRequestTracker { private railsContext: RailsContextWithServerComponentMetadata; - constructor(railsContext: RailsContextWithServerComponentMetadata) { + private generateRSCPayload?: GenerateRSCPayloadFunction; + + constructor( + railsContext: RailsContextWithServerComponentMetadata, + generateRSCPayload?: GenerateRSCPayloadFunction, + ) { this.railsContext = railsContext; + this.generateRSCPayload = generateRSCPayload; } /** @@ -120,17 +110,17 @@ class RSCRequestTracker { * @throws Error if generateRSCPayload is not available or fails */ async getRSCPayloadStream(componentName: string, props: unknown): Promise { - // Validate that the global generateRSCPayload function is available - if (typeof generateRSCPayload !== 'function') { + // Validate that the generateRSCPayload function is available + if (!this.generateRSCPayload) { throw new Error( - 'generateRSCPayload is not defined. Please ensure that you are using at least version 4.0.0 of ' + - 'React on Rails Pro and the Node renderer, and that ReactOnRailsPro.configuration.enable_rsc_support ' + - 'is set to true.', + 'generateRSCPayload function is not available. This could mean: ' + + '(1) ReactOnRailsPro.configuration.enable_rsc_support is not enabled, or ' + + '(2) You are using an incompatible version of React on Rails Pro (requires 4.0.0+).', ); } try { - const stream = await generateRSCPayload(componentName, props, this.railsContext); + const stream = await this.generateRSCPayload(componentName, props, this.railsContext); // Tee stream to allow for multiple consumers: // 1. stream1 - Used by React's runtime to perform server-side rendering diff --git a/packages/react-on-rails-pro/src/streamingUtils.ts b/packages/react-on-rails-pro/src/streamingUtils.ts index f4d6c76eec..12f7f8f7da 100644 --- a/packages/react-on-rails-pro/src/streamingUtils.ts +++ b/packages/react-on-rails-pro/src/streamingUtils.ts @@ -181,11 +181,19 @@ export const streamServerRenderedComponent = ( renderStrategy: StreamRenderer, handleError: (options: ErrorOptions) => PipeableOrReadableStream, ): T => { - const { name: componentName, domNodeId, trace, props, railsContext, throwJsErrors } = options; + const { + name: componentName, + domNodeId, + trace, + props, + railsContext, + throwJsErrors, + generateRSCPayload, + } = options; assertRailsContextWithServerComponentMetadata(railsContext); const postSSRHookTracker = new PostSSRHookTracker(); - const rscRequestTracker = new RSCRequestTracker(railsContext); + const rscRequestTracker = new RSCRequestTracker(railsContext, generateRSCPayload); const streamingTrackers = { postSSRHookTracker, rscRequestTracker, diff --git a/packages/react-on-rails/src/types/index.ts b/packages/react-on-rails/src/types/index.ts index c58fc811ae..a39581cdae 100644 --- a/packages/react-on-rails/src/types/index.ts +++ b/packages/react-on-rails/src/types/index.ts @@ -216,11 +216,18 @@ export interface RegisteredComponent { export type ItemRegistrationCallback = (component: T) => void; +export type GenerateRSCPayloadFunction = ( + componentName: string, + props: unknown, + railsContext: RailsContextWithServerComponentMetadata, +) => Promise; + interface Params { props?: Record; railsContext?: RailsContext; domNodeId?: string; trace?: boolean; + generateRSCPayload?: GenerateRSCPayloadFunction; } export interface RenderParams extends Params { diff --git a/react_on_rails_pro/lib/react_on_rails_pro/server_rendering_js_code.rb b/react_on_rails_pro/lib/react_on_rails_pro/server_rendering_js_code.rb index 7e93544b2b..ff2da52ee9 100644 --- a/react_on_rails_pro/lib/react_on_rails_pro/server_rendering_js_code.rb +++ b/react_on_rails_pro/lib/react_on_rails_pro/server_rendering_js_code.rb @@ -37,13 +37,11 @@ def generate_rsc_payload_js_function(render_options) rscBundleHash: '#{ReactOnRailsPro::Utils.rsc_bundle_hash}', } const runOnOtherBundle = globalThis.runOnOtherBundle; - if (typeof generateRSCPayload !== 'function') { - globalThis.generateRSCPayload = function generateRSCPayload(componentName, props, railsContext) { - const { renderingRequest, rscBundleHash } = railsContext.serverSideRSCPayloadParameters; - const propsString = JSON.stringify(props); - const newRenderingRequest = renderingRequest.replace(/\\(\\s*\\)\\s*$/, `('${componentName}', ${propsString})`); - return runOnOtherBundle(rscBundleHash, newRenderingRequest); - } + const generateRSCPayload = function generateRSCPayload(componentName, props, railsContext) { + const { renderingRequest, rscBundleHash } = railsContext.serverSideRSCPayloadParameters; + const propsString = JSON.stringify(props); + const newRenderingRequest = renderingRequest.replace(/\\(\\s*\\)\\s*$/, `('${componentName}', ${propsString})`); + return runOnOtherBundle(rscBundleHash, newRenderingRequest); } JS end @@ -94,6 +92,7 @@ def render(props_string, rails_context, redux_stores, react_component_name, rend railsContext: railsContext, throwJsErrors: #{ReactOnRailsPro.configuration.throw_js_errors}, renderingReturnsPromises: #{ReactOnRailsPro.configuration.rendering_returns_promises}, + generateRSCPayload: typeof generateRSCPayload !== 'undefined' ? generateRSCPayload : undefined, }); })() JS diff --git a/react_on_rails_pro/packages/node-renderer/tests/concurrentHtmlStreaming.test.ts b/react_on_rails_pro/packages/node-renderer/tests/concurrentHtmlStreaming.test.ts index b4795815c7..59944430bf 100644 --- a/react_on_rails_pro/packages/node-renderer/tests/concurrentHtmlStreaming.test.ts +++ b/react_on_rails_pro/packages/node-renderer/tests/concurrentHtmlStreaming.test.ts @@ -5,9 +5,10 @@ import parser from 'node-html-parser'; // @ts-expect-error TODO: fix later import { RSCPayloadChunk } from 'react-on-rails'; import buildApp from '../src/worker'; -import config from './testingNodeRendererConfigs'; +import { createTestConfig } from './testingNodeRendererConfigs'; import { makeRequest } from './httpRequestUtils'; +const { config } = createTestConfig('concurrentHtmlStreaming'); const app = buildApp(config); const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; const redisClient = createClient({ url: redisUrl }); diff --git a/react_on_rails_pro/packages/node-renderer/tests/fixtures/projects/spec-dummy/asyncComponentsTreeForTestingRenderingRequest.js b/react_on_rails_pro/packages/node-renderer/tests/fixtures/projects/spec-dummy/asyncComponentsTreeForTestingRenderingRequest.js index 93417a927a..4a7420982c 100644 --- a/react_on_rails_pro/packages/node-renderer/tests/fixtures/projects/spec-dummy/asyncComponentsTreeForTestingRenderingRequest.js +++ b/react_on_rails_pro/packages/node-renderer/tests/fixtures/projects/spec-dummy/asyncComponentsTreeForTestingRenderingRequest.js @@ -9,13 +9,11 @@ } const runOnOtherBundle = globalThis.runOnOtherBundle; - if (typeof generateRSCPayload !== 'function') { - globalThis.generateRSCPayload = function generateRSCPayload(componentName, props, railsContext) { - const { renderingRequest, rscBundleHash } = railsContext.serverSideRSCPayloadParameters; - const propsString = JSON.stringify(props); - const newRenderingRequest = renderingRequest.replace(/\(\s*\)\s*$/, `('${componentName}', ${propsString})`); - return runOnOtherBundle(rscBundleHash, newRenderingRequest); - } + const generateRSCPayload = function generateRSCPayload(componentName, props, railsContext) { + const { renderingRequest, rscBundleHash } = railsContext.serverSideRSCPayloadParameters; + const propsString = JSON.stringify(props); + const newRenderingRequest = renderingRequest.replace(/\(\s*\)\s*$/, `('${componentName}', ${propsString})`); + return runOnOtherBundle(rscBundleHash, newRenderingRequest); } ReactOnRails.clearHydratedStores(); @@ -35,5 +33,6 @@ railsContext: railsContext, throwJsErrors: false, renderingReturnsPromises: true, + generateRSCPayload: typeof generateRSCPayload !== 'undefined' ? generateRSCPayload : undefined, }); })() diff --git a/react_on_rails_pro/packages/node-renderer/tests/htmlStreaming.test.js b/react_on_rails_pro/packages/node-renderer/tests/htmlStreaming.test.js index cd59220570..45b98e683d 100644 --- a/react_on_rails_pro/packages/node-renderer/tests/htmlStreaming.test.js +++ b/react_on_rails_pro/packages/node-renderer/tests/htmlStreaming.test.js @@ -1,9 +1,10 @@ import http2 from 'http2'; import buildApp from '../src/worker'; -import config from './testingNodeRendererConfigs'; +import { createTestConfig } from './testingNodeRendererConfigs'; import * as errorReporter from '../src/shared/errorReporter'; import { createForm, SERVER_BUNDLE_TIMESTAMP } from './httpRequestUtils'; +const { config } = createTestConfig('htmlStreaming'); const app = buildApp(config); beforeAll(async () => { diff --git a/react_on_rails_pro/packages/node-renderer/tests/httpRequestUtils.ts b/react_on_rails_pro/packages/node-renderer/tests/httpRequestUtils.ts index 4669c6431b..a1a8133db6 100644 --- a/react_on_rails_pro/packages/node-renderer/tests/httpRequestUtils.ts +++ b/react_on_rails_pro/packages/node-renderer/tests/httpRequestUtils.ts @@ -237,7 +237,7 @@ export const getNextChunkInternal = ( stream.once('data', onData); stream.once('error', onError); - if (stream.closed) { + if ('closed' in stream && stream.closed) { onClose(); } else { stream.once('close', onClose); diff --git a/react_on_rails_pro/packages/node-renderer/tests/incrementalHtmlStreaming.test.ts b/react_on_rails_pro/packages/node-renderer/tests/incrementalHtmlStreaming.test.ts index 01facda6d0..750cdd2ded 100644 --- a/react_on_rails_pro/packages/node-renderer/tests/incrementalHtmlStreaming.test.ts +++ b/react_on_rails_pro/packages/node-renderer/tests/incrementalHtmlStreaming.test.ts @@ -1,7 +1,6 @@ import http2 from 'http2'; -import * as fs from 'fs'; import buildApp from '../src/worker'; -import config, { BUNDLE_PATH } from './testingNodeRendererConfigs'; +import { createTestConfig } from './testingNodeRendererConfigs'; import * as errorReporter from '../src/shared/errorReporter'; import { createRenderingRequest, @@ -13,12 +12,10 @@ import { } from './httpRequestUtils'; import packageJson from '../src/shared/packageJson'; +const { config } = createTestConfig('incrementalHtmlStreaming'); const app = buildApp(config); beforeAll(async () => { - if (fs.existsSync(BUNDLE_PATH)) { - fs.rmSync(BUNDLE_PATH, { recursive: true, force: true }); - } await app.ready(); await app.listen({ port: 0 }); }); @@ -161,8 +158,7 @@ it('incremental render html', async () => { close(); }); -// TODO: fix the problem of having a global shared `runOnOtherBundle` function -it.skip('raises an error if a specific async prop is not sent', async () => { +it('raises an error if a specific async prop is not sent', async () => { const { status, body } = await makeRequest(); expect(body).toBe(''); expect(status).toBe(200); @@ -182,3 +178,56 @@ it.skip('raises an error if a specific async prop is not sent', async () => { await expect(getNextChunk(request)).rejects.toThrow('Stream Closed'); close(); }); + +describe('concurrent incremental HTML streaming', () => { + it('handles multiple parallel requests without race conditions', async () => { + await makeRequest(); + + const numRequests = 5; + const requests = []; + + // Start all requests + for (let i = 0; i < numRequests; i += 1) { + const { request, close } = createHttpRequest(RSC_BUNDLE_TIMESTAMP, `concurrent-test-${i}`); + request.write(`${JSON.stringify(createInitialObject())}\n`); + requests.push({ request, close, id: i }); + } + + // Wait for all to connect and get initial chunks + await Promise.all(requests.map(({ request }) => waitForStatus(request))); + await Promise.all(requests.map(({ request }) => getNextChunk(request))); + + // Send update chunks to ALL requests before waiting for any responses + // If sequential: second request wouldn't process until first completes + // If concurrent: all process simultaneously + requests.forEach(({ request, id }) => { + request.write( + `${JSON.stringify({ + bundleTimestamp: RSC_BUNDLE_TIMESTAMP, + updateChunk: ` + (function(){ + var asyncPropsManager = sharedExecutionContext.get("asyncPropsManager"); + asyncPropsManager.setProp("books", ["Request-${id}-Book"]); + asyncPropsManager.setProp("researches", ["Request-${id}-Research"]); + })() + `, + })}\n`, + ); + request.end(); + }); + + // Now wait for all responses - they should all succeed + const results = await Promise.all( + requests.map(async ({ request, close, id }) => { + const chunk = await getNextChunk(request); + close(); + return { id, chunk }; + }), + ); + + results.forEach(({ id, chunk }) => { + expect(chunk).toContain(`Request-${id}-Book`); + expect(chunk).toContain(`Request-${id}-Research`); + }); + }); +}); diff --git a/react_on_rails_pro/packages/node-renderer/tests/incrementalRender.test.ts b/react_on_rails_pro/packages/node-renderer/tests/incrementalRender.test.ts index e0a79a895e..e85df9f637 100644 --- a/react_on_rails_pro/packages/node-renderer/tests/incrementalRender.test.ts +++ b/react_on_rails_pro/packages/node-renderer/tests/incrementalRender.test.ts @@ -67,12 +67,14 @@ describe('incremental render NDJSON endpoint', () => { const createMockSink = () => { const sinkAdd = jest.fn(); + const handleRequestClosed = jest.fn(); const sink: incremental.IncrementalRenderSink = { add: sinkAdd, + handleRequestClosed, }; - return { sink, sinkAdd }; + return { sink, sinkAdd, handleRequestClosed }; }; const createMockResponse = (data = 'mock response'): ResponseResult => ({ @@ -124,7 +126,7 @@ describe('incremental render NDJSON endpoint', () => { const createBasicTestSetup = async () => { await createVmBundle(TEST_NAME); - const { sink, sinkAdd } = createMockSink(); + const { sink, sinkAdd, handleRequestClosed } = createMockSink(); const mockResponse = createMockResponse(); const mockResult = createMockResult(sink, mockResponse); @@ -137,6 +139,7 @@ describe('incremental render NDJSON endpoint', () => { return { sink, sinkAdd, + handleRequestClosed, mockResponse, mockResult, handleSpy, @@ -158,9 +161,11 @@ describe('incremental render NDJSON endpoint', () => { }); const sinkAdd = jest.fn(); + const handleRequestClosed = jest.fn(); const sink: incremental.IncrementalRenderSink = { add: sinkAdd, + handleRequestClosed, }; const mockResponse: ResponseResult = { @@ -183,6 +188,7 @@ describe('incremental render NDJSON endpoint', () => { return { responseStream, sinkAdd, + handleRequestClosed, sink, mockResponse, mockResult, @@ -256,7 +262,7 @@ describe('incremental render NDJSON endpoint', () => { }); test('calls handleIncrementalRenderRequest immediately after first chunk and processes each subsequent chunk immediately', async () => { - const { sinkAdd, handleSpy, SERVER_BUNDLE_TIMESTAMP } = await createBasicTestSetup(); + const { sinkAdd, handleRequestClosed, handleSpy, SERVER_BUNDLE_TIMESTAMP } = await createBasicTestSetup(); // Create the HTTP request const req = createHttpRequest(SERVER_BUNDLE_TIMESTAMP); @@ -304,6 +310,11 @@ describe('incremental render NDJSON endpoint', () => { // Final verification: all chunks were processed in the correct order expect(handleSpy).toHaveBeenCalledTimes(1); expect(sinkAdd.mock.calls).toEqual([[{ a: 1 }], [{ b: 2 }], [{ c: 3 }]]); + + // Verify handleRequestClosed was called when connection closed + await waitFor(() => { + expect(handleRequestClosed).toHaveBeenCalledTimes(1); + }); }); test('returns 410 error when bundle is missing', async () => { @@ -357,7 +368,7 @@ describe('incremental render NDJSON endpoint', () => { // Create a bundle for this test await createVmBundle(TEST_NAME); - const { sink, sinkAdd } = createMockSink(); + const { sink, sinkAdd, handleRequestClosed } = createMockSink(); const mockResponse: ResponseResult = createMockResponse(); @@ -416,13 +427,16 @@ describe('incremental render NDJSON endpoint', () => { await waitFor(() => { expect(sinkAdd.mock.calls).toEqual([[{ a: 1 }], [{ d: 4 }]]); }); + + // Verify handleRequestClosed was called when connection closed + expect(handleRequestClosed).toHaveBeenCalledTimes(1); }); test('handles empty lines gracefully in the stream', async () => { // Create a bundle for this test await createVmBundle(TEST_NAME); - const { sink, sinkAdd } = createMockSink(); + const { sink, sinkAdd, handleRequestClosed } = createMockSink(); const mockResponse: ResponseResult = createMockResponse(); @@ -469,6 +483,11 @@ describe('incremental render NDJSON endpoint', () => { // Verify that only valid JSON objects were processed expect(handleSpy).toHaveBeenCalledTimes(1); expect(sinkAdd.mock.calls).toEqual([[{ a: 1 }], [{ b: 2 }], [{ c: 3 }]]); + + // Verify handleRequestClosed was called when connection closed + await waitFor(() => { + expect(handleRequestClosed).toHaveBeenCalledTimes(1); + }); }); test('throws error when first chunk processing fails (e.g., authentication)', async () => { @@ -515,7 +534,8 @@ describe('incremental render NDJSON endpoint', () => { 'Goodbye from stream', ]; - const { responseStream, sinkAdd, handleSpy, SERVER_BUNDLE_TIMESTAMP } = await createStreamingTestSetup(); + const { responseStream, sinkAdd, handleRequestClosed, handleSpy, SERVER_BUNDLE_TIMESTAMP } = + await createStreamingTestSetup(); // write the response chunks to the stream let sentChunkIndex = 0; @@ -586,10 +606,14 @@ describe('incremental render NDJSON endpoint', () => { // Verify that the mock was called correctly expect(handleSpy).toHaveBeenCalledTimes(1); + + // Verify handleRequestClosed was called when connection closed + expect(handleRequestClosed).toHaveBeenCalledTimes(1); }); test('echo server - processes each chunk and immediately streams it back', async () => { - const { responseStream, sinkAdd, handleSpy, SERVER_BUNDLE_TIMESTAMP } = await createStreamingTestSetup(); + const { responseStream, sinkAdd, handleRequestClosed, handleSpy, SERVER_BUNDLE_TIMESTAMP } = + await createStreamingTestSetup(); // Create the HTTP request const req = createHttpRequest(SERVER_BUNDLE_TIMESTAMP); @@ -677,6 +701,9 @@ describe('incremental render NDJSON endpoint', () => { // Verify that the mock was called correctly expect(handleSpy).toHaveBeenCalledTimes(1); + + // Verify handleRequestClosed was called when connection closed + expect(handleRequestClosed).toHaveBeenCalledTimes(1); }); describe('incremental render update chunk functionality', () => { diff --git a/react_on_rails_pro/packages/node-renderer/tests/testingNodeRendererConfigs.ts b/react_on_rails_pro/packages/node-renderer/tests/testingNodeRendererConfigs.ts index d236f84cce..99c65616e9 100644 --- a/react_on_rails_pro/packages/node-renderer/tests/testingNodeRendererConfigs.ts +++ b/react_on_rails_pro/packages/node-renderer/tests/testingNodeRendererConfigs.ts @@ -3,36 +3,49 @@ import { env } from 'process'; import { LevelWithSilent } from 'pino'; import { Config } from '../src/shared/configBuilder'; -export const BUNDLE_PATH = './tmp/node-renderer-bundles-test'; -if (fs.existsSync(BUNDLE_PATH)) { - fs.rmSync(BUNDLE_PATH, { recursive: true, force: true }); +/** + * Creates a test configuration with a unique bundle path for each test file. + * This prevents race conditions when tests run in parallel. + * + * @param testName - Unique identifier for the test (e.g., test file name) + * @returns Config object with unique serverBundleCachePath + */ +export function createTestConfig(testName: string): { config: Partial; bundlePath: string } { + const bundlePath = `./tmp/node-renderer-bundles-test-${testName}`; + + // Clean up any existing directory + if (fs.existsSync(bundlePath)) { + fs.rmSync(bundlePath, { recursive: true, force: true }); + } + + const config: Partial = { + // This is the default but avoids searching for the Rails root + serverBundleCachePath: bundlePath, + port: (env.RENDERER_PORT && parseInt(env.RENDERER_PORT, 10)) || 3800, // Listen at RENDERER_PORT env value or default port 3800 + logLevel: (env.RENDERER_LOG_LEVEL as LevelWithSilent | undefined) || 'info', + + // See value in /config/initializers/react_on_rails_pro.rb. Should use env value in real app. + password: 'myPassword1', + + // If set to true, `supportModules` enables the server-bundle code to call a default set of NodeJS modules + // that get added to the VM context: { Buffer, process, setTimeout, setInterval, clearTimeout, clearInterval }. + // This option is required to equal `true` if you want to use loadable components. + // Setting this value to false causes the NodeRenderer to behave like ExecJS + supportModules: true, + + // additionalContext enables you to specify additional NodeJS modules to add to the VM context in + // addition to our supportModules defaults. + additionalContext: { URL, AbortController }, + + // Required to use setTimeout, setInterval, & clearTimeout during server rendering + stubTimers: false, + + // If set to true, replayServerAsyncOperationLogs will replay console logs from async server operations. + // If set to false, replayServerAsyncOperationLogs will replay console logs from sync server operations only. + replayServerAsyncOperationLogs: true, + }; + + return { config, bundlePath }; } -const config: Partial = { - // This is the default but avoids searching for the Rails root - serverBundleCachePath: BUNDLE_PATH, - port: (env.RENDERER_PORT && parseInt(env.RENDERER_PORT, 10)) || 3800, // Listen at RENDERER_PORT env value or default port 3800 - logLevel: (env.RENDERER_LOG_LEVEL as LevelWithSilent | undefined) || 'info', - - // See value in /config/initializers/react_on_rails_pro.rb. Should use env value in real app. - password: 'myPassword1', - - // If set to true, `supportModules` enables the server-bundle code to call a default set of NodeJS modules - // that get added to the VM context: { Buffer, process, setTimeout, setInterval, clearTimeout, clearInterval }. - // This option is required to equal `true` if you want to use loadable components. - // Setting this value to false causes the NodeRenderer to behave like ExecJS - supportModules: true, - - // additionalContext enables you to specify additional NodeJS modules to add to the VM context in - // addition to our supportModules defaults. - additionalContext: { URL, AbortController }, - - // Required to use setTimeout, setInterval, & clearTimeout during server rendering - stubTimers: false, - - // If set to true, replayServerAsyncOperationLogs will replay console logs from async server operations. - // If set to false, replayServerAsyncOperationLogs will replay console logs from sync server operations only. - replayServerAsyncOperationLogs: true, -}; - -export default config; +export default createTestConfig; diff --git a/react_on_rails_pro/packages/node-renderer/tests/worker.test.ts b/react_on_rails_pro/packages/node-renderer/tests/worker.test.ts index 644e7cb4ce..05c6b1dd24 100644 --- a/react_on_rails_pro/packages/node-renderer/tests/worker.test.ts +++ b/react_on_rails_pro/packages/node-renderer/tests/worker.test.ts @@ -893,7 +893,7 @@ describe('worker', () => { 400, ); - expect(res.payload).toContain('INVALID NIL or NULL result for rendering'); + expect(res.payload).toContain('Invalid first incremental render request chunk received'); }); test('fails when password is missing', async () => {