react_on_rails_pro 16.4.0.rc.6 → 16.4.0.rc.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d41e1dcb19326994f54698beebb6194a198eb967beea7346b08a376fb86068fd
4
- data.tar.gz: 98ca3512787b5d444266eb0cac44420b6a991aab2d928305c8640c752b3981d3
3
+ metadata.gz: d8d5365a9653da4cdc8ea771167d5b26dd03602c4977d7fea747ec2a07cc6900
4
+ data.tar.gz: 57afd9dc58238f94d2d008cc01900596a18cb63894786cb60ce99471902500c9
5
5
  SHA512:
6
- metadata.gz: a802fe1da5ebcd4571d4c75d5df49dec8d4894ac298c73e376f31ee34a12faaa0db4b78d137c96932354e366dfbc7ac168c79bcf911721f709e4f5ba52078592
7
- data.tar.gz: 31d4f3e670a195d169aa256876a02802341235c439dc9f815f8216c4db8bfbdca933d31acf8e69e845c2e88fa4e897cc49139e89a03a02379eca55acc91735dd
6
+ metadata.gz: fb6bb5c6f0b45d62151be232c4ddf9b8343aaa086162e1f8292f9468b2902c146a5668c1b1e79753a919a6063eae8ddbf1a19ad4cb8e178734e77fb9303efa1d
7
+ data.tar.gz: 9da57b6e9750c1f5fe4f830b375bfd2dd7e7e07b6c2135b5e9a30ec5156f70c4e41180a8737a205150bf7b8687dfa1b16c035bb88eb03fd3d89dbdc71e25c625
data/CONTRIBUTING.md CHANGED
@@ -119,7 +119,7 @@ script/ci-changes-detector origin/master
119
119
  - Push Pro changes without testing locally first
120
120
  - Modify both Pro and main gem without running full tests
121
121
 
122
- For comprehensive CI documentation, see [`../docs/contributor-info/ci-optimization.md`](../docs/contributor-info/ci-optimization.md) in the repository root.
122
+ For comprehensive CI documentation, see [`../internal/contributor-info/ci-optimization.md`](../internal/contributor-info/ci-optimization.md) in the repository root.
123
123
 
124
124
  # IDE/Editor Setup
125
125
 
@@ -395,5 +395,5 @@ rake release[17.0.0,false,verdaccio]
395
395
 
396
396
  For complete documentation, see:
397
397
 
398
- - [Root Release Documentation](../docs/contributor-info/releasing.md)
398
+ - [Root Release Documentation](../internal/contributor-info/releasing.md)
399
399
  - Run `rake -D release` for inline help
@@ -8,7 +8,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
8
8
 
9
9
  gem "react_on_rails", path: "../"
10
10
 
11
- gem "shakapacker", "9.5.0"
11
+ gem "shakapacker", "9.6.1"
12
12
  gem "bootsnap", require: false
13
13
  gem "rails", "~> 7.1"
14
14
  gem "puma", "~> 6"
data/Gemfile.lock CHANGED
@@ -9,7 +9,7 @@ GIT
9
9
  PATH
10
10
  remote: ..
11
11
  specs:
12
- react_on_rails (16.4.0.rc.6)
12
+ react_on_rails (16.4.0.rc.7)
13
13
  addressable
14
14
  connection_pool
15
15
  execjs (~> 2.5)
@@ -20,7 +20,7 @@ PATH
20
20
  PATH
21
21
  remote: .
22
22
  specs:
23
- react_on_rails_pro (16.4.0.rc.6)
23
+ react_on_rails_pro (16.4.0.rc.7)
24
24
  addressable
25
25
  async (>= 2.6)
26
26
  connection_pool
@@ -29,7 +29,7 @@ PATH
29
29
  httpx (~> 1.5)
30
30
  jwt (~> 2.7)
31
31
  rainbow
32
- react_on_rails (= 16.4.0.rc.6)
32
+ react_on_rails (= 16.4.0.rc.7)
33
33
 
34
34
  GEM
35
35
  remote: https://rubygems.org/
@@ -165,7 +165,7 @@ GEM
165
165
  drb (2.2.3)
166
166
  equivalent-xml (0.6.0)
167
167
  nokogiri (>= 1.4.3)
168
- erb (6.0.1)
168
+ erb (6.0.2)
169
169
  erubi (1.13.1)
170
170
  execjs (2.10.0)
171
171
  fakefs (2.8.0)
@@ -194,8 +194,9 @@ GEM
194
194
  concurrent-ruby (~> 1.0)
195
195
  io-console (0.8.2)
196
196
  io-event (1.14.2)
197
- irb (1.16.0)
197
+ irb (1.17.0)
198
198
  pp (>= 0.6.0)
199
+ prism (>= 1.3.0)
199
200
  rdoc (>= 4.0.0)
200
201
  reline (>= 0.4.2)
201
202
  jbuilder (2.12.0)
@@ -230,7 +231,8 @@ GEM
230
231
  method_source (1.1.0)
231
232
  metrics (0.15.0)
232
233
  mini_mime (1.1.5)
233
- minitest (6.0.1)
234
+ minitest (6.0.2)
235
+ drb (~> 2.0)
234
236
  prism (~> 1.5)
235
237
  mize (0.4.1)
236
238
  protocol (~> 2.0)
@@ -247,11 +249,11 @@ GEM
247
249
  net-smtp (0.5.1)
248
250
  net-protocol
249
251
  nio4r (2.7.5)
250
- nokogiri (1.19.0-arm64-darwin)
252
+ nokogiri (1.19.1-arm64-darwin)
251
253
  racc (~> 1.4)
252
- nokogiri (1.19.0-x86_64-darwin)
254
+ nokogiri (1.19.1-x86_64-darwin)
253
255
  racc (~> 1.4)
254
- nokogiri (1.19.0-x86_64-linux-gnu)
256
+ nokogiri (1.19.1-x86_64-linux-gnu)
255
257
  racc (~> 1.4)
256
258
  package_json (0.2.0)
257
259
  parallel (1.27.0)
@@ -262,7 +264,7 @@ GEM
262
264
  pp (0.6.3)
263
265
  prettyprint
264
266
  prettyprint (0.2.0)
265
- prism (1.6.0)
267
+ prism (1.9.0)
266
268
  protocol (2.0.0)
267
269
  ruby_parser (~> 3.0)
268
270
  pry (0.14.2)
@@ -282,7 +284,7 @@ GEM
282
284
  puma (6.5.0)
283
285
  nio4r (~> 2.0)
284
286
  racc (1.8.1)
285
- rack (3.2.4)
287
+ rack (3.2.5)
286
288
  rack-proxy (0.7.7)
287
289
  rack
288
290
  rack-session (2.1.1)
@@ -310,8 +312,8 @@ GEM
310
312
  activesupport (>= 5.0.0)
311
313
  minitest
312
314
  nokogiri (>= 1.6)
313
- rails-html-sanitizer (1.6.2)
314
- loofah (~> 2.21)
315
+ rails-html-sanitizer (1.7.0)
316
+ loofah (~> 2.25)
315
317
  nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
316
318
  railties (7.2.3)
317
319
  actionpack (= 7.2.3)
@@ -330,7 +332,7 @@ GEM
330
332
  ffi (~> 1.0)
331
333
  rbs (3.9.5)
332
334
  logger
333
- rdoc (7.1.0)
335
+ rdoc (7.2.0)
334
336
  erb
335
337
  psych (>= 4.0.0)
336
338
  tsort
@@ -414,9 +416,9 @@ GEM
414
416
  rexml (~> 3.2, >= 3.2.5)
415
417
  rubyzip (>= 1.2.2, < 3.0)
416
418
  websocket (~> 1.0)
417
- semantic_range (3.1.0)
419
+ semantic_range (3.1.1)
418
420
  sexp_processor (4.17.1)
419
- shakapacker (9.5.0)
421
+ shakapacker (9.6.1)
420
422
  activesupport (>= 5.2)
421
423
  package_json
422
424
  rack-proxy (>= 0.6.1)
@@ -483,7 +485,7 @@ GEM
483
485
  xpath (3.2.0)
484
486
  nokogiri (~> 1.8)
485
487
  yard (0.9.36)
486
- zeitwerk (2.7.4)
488
+ zeitwerk (2.7.5)
487
489
 
488
490
  PLATFORMS
489
491
  arm64-darwin-24
@@ -531,7 +533,7 @@ DEPENDENCIES
531
533
  sass-rails
532
534
  scss_lint
533
535
  selenium-webdriver (= 4.9.0)
534
- shakapacker (= 9.5.0)
536
+ shakapacker (= 9.6.1)
535
537
  spring
536
538
  spring-watcher-listen
537
539
  sprockets
@@ -314,7 +314,13 @@ module ReactOnRailsProHelper
314
314
 
315
315
  # Enqueue remaining chunks asynchronously
316
316
  @async_barrier.async do
317
- rest_chunks.each { |chunk| @main_output_queue.enqueue(chunk) }
317
+ rest_chunks.each do |chunk|
318
+ break if response.stream.closed?
319
+
320
+ @main_output_queue.enqueue(chunk)
321
+ end
322
+ rescue Async::Queue::ClosedError
323
+ # Queue closed due to error/disconnect in another component — stop enqueuing
318
324
  end
319
325
 
320
326
  # Return first chunk directly
@@ -436,8 +442,8 @@ module ReactOnRailsProHelper
436
442
  # Start an async task on the barrier to stream all chunks
437
443
  @async_barrier.async do
438
444
  stream = yield
439
- process_stream_chunks(stream, first_chunk_var, all_chunks)
440
- on_complete&.call(all_chunks)
445
+ fully_consumed = process_stream_chunks(stream, first_chunk_var, all_chunks)
446
+ on_complete&.call(all_chunks) if fully_consumed
441
447
  rescue StandardError => e
442
448
  # Propagate the error to the calling fiber via the variable.
443
449
  # Async::Variable can only be resolved once — if it was already resolved
@@ -457,9 +463,9 @@ module ReactOnRailsProHelper
457
463
  end
458
464
  end
459
465
 
460
- # Wait for and return the first chunk (blocking)
461
- first_chunk_var.wait
462
- result = first_chunk_var.value
466
+ # Wait for and return the first chunk (blocking).
467
+ # Async::Variable#wait blocks until resolved, then returns the stored value.
468
+ result = first_chunk_var.wait
463
469
 
464
470
  # If the async task stored an exception (pre-first-chunk error), raise it now.
465
471
  # This happens BEFORE response.stream.write(template_string) in
@@ -470,12 +476,17 @@ module ReactOnRailsProHelper
470
476
  result
471
477
  end
472
478
 
479
+ # Returns true if the stream was fully consumed, false if aborted (client disconnect).
480
+ # When false, callers must NOT invoke on_complete to avoid caching partial data.
473
481
  def process_stream_chunks(stream, first_chunk_var, all_chunks)
474
482
  is_first = true
475
483
 
476
484
  stream.each_chunk do |chunk|
477
- # Check if client disconnected before processing chunk
478
- break if response.stream.closed?
485
+ # Client disconnected abort without caching partial results
486
+ if response.stream.closed?
487
+ first_chunk_var.value = nil if is_first
488
+ return false
489
+ end
479
490
 
480
491
  all_chunks&.push(chunk)
481
492
 
@@ -491,6 +502,7 @@ module ReactOnRailsProHelper
491
502
 
492
503
  # Handle case where stream has no chunks
493
504
  first_chunk_var.value = nil if is_first
505
+ true
494
506
  end
495
507
 
496
508
  def internal_stream_react_component(component_name, options = {})
data/docs/installation.md CHANGED
@@ -8,7 +8,8 @@ Check the [CHANGELOG](https://github.com/shakacode/react_on_rails/blob/master/CH
8
8
 
9
9
  ## Version Format
10
10
 
11
- For the below docs, find the desired `<version>` in the CHANGELOG. Note that for pre-release versions:
11
+ For the commands below, choose versions from the CHANGELOG and replace placeholders like
12
+ `<gem_version>` and `<npm_version>`. Note that for pre-release versions:
12
13
 
13
14
  - Gems use all periods: `16.2.0.beta.1`
14
15
  - NPM packages use dashes: `16.2.0-beta.1`
@@ -22,11 +23,15 @@ The easiest way to set up React on Rails Pro is using the generator. This automa
22
23
  For new React on Rails apps, use the `--pro` flag:
23
24
 
24
25
  ```bash
25
- # Add the Pro gem to your Gemfile first
26
- bundle add react_on_rails_pro
26
+ # Add the Pro gem first (pin exact version)
27
+ bundle add react_on_rails_pro --version="<gem_version>" --strict
28
+
29
+ # The generator requires a clean git working tree
30
+ git add .
31
+ git commit -m "Prepare app for React on Rails Pro install"
27
32
 
28
33
  # Run the generator with --pro
29
- rails generate react_on_rails:install --pro
34
+ bundle exec rails generate react_on_rails:install --pro
30
35
  ```
31
36
 
32
37
  This creates the Pro initializer, node-renderer.js, installs npm packages, and adds the Node Renderer to Procfile.dev.
@@ -37,17 +42,31 @@ For existing React on Rails apps, use the standalone Pro generator:
37
42
 
38
43
  ```bash
39
44
  # Add the Pro gem to your Gemfile
40
- bundle add react_on_rails_pro
45
+ bundle add react_on_rails_pro --version="<gem_version>" --strict
41
46
 
42
47
  # Run the Pro generator
43
- rails generate react_on_rails:pro
48
+ bundle exec rails generate react_on_rails:pro
44
49
  ```
45
50
 
46
51
  The standalone generator adds Pro-specific files and modifies your existing webpack configs (`serverWebpackConfig.js` and `ServerClientOrBoth.js`) to enable Pro features like `libraryTarget: 'commonjs2'` and `target = 'node'`.
47
52
 
48
53
  ## After Running the Generator
49
54
 
50
- You still need to configure your license. Set the environment variable:
55
+ Run a quick validation, then configure your license.
56
+
57
+ ```bash
58
+ bundle exec rails react_on_rails:doctor
59
+ bin/shakapacker
60
+ bin/dev
61
+ ```
62
+
63
+ If port 3000 is already in use:
64
+
65
+ ```bash
66
+ PORT=3001 bin/dev
67
+ ```
68
+
69
+ Set the license environment variable:
51
70
 
52
71
  ```bash
53
72
  export REACT_ON_RAILS_PRO_LICENSE="your-license-token-here"
@@ -61,10 +80,10 @@ RSC requires React on Rails Pro and React 19.0.x. To add RSC support, use `--rsc
61
80
 
62
81
  ```bash
63
82
  # Fresh install with RSC
64
- rails generate react_on_rails:install --rsc
83
+ bundle exec rails generate react_on_rails:install --rsc
65
84
 
66
85
  # Or add RSC to existing Pro app
67
- rails generate react_on_rails:rsc
86
+ bundle exec rails generate react_on_rails:rsc
68
87
  ```
69
88
 
70
89
  The RSC generator creates `rscWebpackConfig.js`, adds `RSCWebpackPlugin` to both server and client webpack configs, configures `RSC_BUNDLE_ONLY` handling in `ServerClientOrBoth.js`, and sets up the RSC bundle watcher process. See [React Server Components](./react-server-components/tutorial.md) for more information.
@@ -86,7 +105,7 @@ Ensure your **Rails** app is using the **react_on_rails** gem, version 16.0.0 or
86
105
  Add the `react_on_rails_pro` gem to your **Gemfile**:
87
106
 
88
107
  ```ruby
89
- gem "react_on_rails_pro", "~> 16.2"
108
+ gem "react_on_rails_pro", "= <gem_version>"
90
109
  ```
91
110
 
92
111
  Then run:
@@ -138,19 +157,19 @@ All React on Rails Pro users need to install the `react-on-rails-pro` npm packag
138
157
  ### Using npm:
139
158
 
140
159
  ```bash
141
- npm install react-on-rails-pro
160
+ npm install react-on-rails-pro@<npm_version> --save-exact
142
161
  ```
143
162
 
144
163
  ### Using yarn:
145
164
 
146
165
  ```bash
147
- yarn add react-on-rails-pro
166
+ yarn add react-on-rails-pro@<npm_version> --exact
148
167
  ```
149
168
 
150
169
  ### Using pnpm:
151
170
 
152
171
  ```bash
153
- pnpm add react-on-rails-pro
172
+ pnpm add react-on-rails-pro@<npm_version> --save-exact
154
173
  ```
155
174
 
156
175
  ## Usage
@@ -187,13 +206,13 @@ See the [React Server Components tutorial](./react-server-components/tutorial.md
187
206
  ### Using npm:
188
207
 
189
208
  ```bash
190
- npm install react-on-rails-pro-node-renderer
209
+ npm install react-on-rails-pro-node-renderer@<npm_version> --save-exact
191
210
  ```
192
211
 
193
212
  ### Using yarn:
194
213
 
195
214
  ```bash
196
- yarn add react-on-rails-pro-node-renderer
215
+ yarn add react-on-rails-pro-node-renderer@<npm_version> --exact
197
216
  ```
198
217
 
199
218
  ### Add to package.json:
@@ -201,7 +220,7 @@ yarn add react-on-rails-pro-node-renderer
201
220
  ```json
202
221
  {
203
222
  "dependencies": {
204
- "react-on-rails-pro-node-renderer": "^16.2.0"
223
+ "react-on-rails-pro-node-renderer": "<npm_version>"
205
224
  }
206
225
  }
207
226
  ```
@@ -9,6 +9,16 @@ A React architecture that allows components to execute exclusively on the server
9
9
  - Improved initial page load
10
10
  - Better SEO
11
11
 
12
+ <!-- H2 is intentional here: this callout must stand out above the H3 glossary entries to prevent confusion -->
13
+ ## Important: `.client.` / `.server.` File Suffixes Are Unrelated
14
+
15
+ React on Rails has a separate, older concept where files can have `.client.jsx` or `.server.jsx` suffixes. These control **which webpack bundle** imports the file (client bundle vs. server bundle for SSR) and have nothing to do with React Server Components.
16
+
17
+ - `Component.client.jsx` → only imported in the client (browser) bundle
18
+ - `Component.server.jsx` → only imported in the server and the RSC bundle
19
+
20
+ A `.server.jsx` file is NOT a React Server Component. A `.client.jsx` file is NOT necessarily a React Client Component. The RSC classification is determined solely by the `'use client'` directive, regardless of file suffix. These suffixes only make sense for client components, as server components exist only in the RSC bundle. See the [auto-bundling docs](../../../docs/core-concepts/auto-bundling-file-system-based-automated-bundle-generation.md#server-rendering-and-client-rendering-components) for details on file suffixes.
21
+
12
22
  ## Types of Components
13
23
 
14
24
  ### Server Components
@@ -155,6 +155,37 @@ For example, with our `MyStreamingComponent`, the sequence might be:
155
155
  </script>
156
156
  ```
157
157
 
158
+ ## Compression Middleware Compatibility
159
+
160
+ Streaming responses use `ActionController::Live`, which writes chunks to a `SizedQueue` (a destructive, non-idempotent data structure). Standard Rack compression middleware (`Rack::Deflater`, `Rack::Brotli`) works correctly with streaming **by default** — each chunk is compressed and flushed immediately, preserving low TTFB.
161
+
162
+ However, if you pass an `:if` condition that calls `body.each` to check the response size, **streaming responses will deadlock**. The `:if` callback destructively consumes all chunks from the queue, leaving nothing for the compressor to read.
163
+
164
+ ```ruby
165
+ # BAD — causes deadlocks with streaming responses
166
+ config.middleware.use Rack::Deflater, if: lambda { |*, body|
167
+ sum = 0
168
+ body.each { |i| sum += i.length } # destructive — drains the queue
169
+ sum > 512
170
+ }
171
+ ```
172
+
173
+ The [Rack SPEC](https://github.com/rack/rack/blob/main/SPEC.rdoc) states that `each` must only be called once and middleware must not call `each` directly unless the body responds to `to_ary`. Streaming bodies explicitly do not support `to_ary`.
174
+
175
+ **Correct pattern** — check `to_ary` before iterating:
176
+
177
+ ```ruby
178
+ config.middleware.use Rack::Deflater, if: lambda { |*, body|
179
+ # Streaming bodies don't support to_ary — always compress them.
180
+ # Rack::Deflater handles streaming correctly with sync flush per chunk.
181
+ return true unless body.respond_to?(:to_ary)
182
+
183
+ body.to_ary.sum(&:bytesize) > 512
184
+ }
185
+ ```
186
+
187
+ The same applies to `Rack::Brotli` or any middleware that accepts an `:if` callback.
188
+
158
189
  ## When to Use Streaming
159
190
 
160
191
  Streaming SSR is particularly valuable in specific scenarios. Here's when to consider it:
@@ -1,3 +1,11 @@
1
1
  # Troubleshooting
2
2
 
3
3
  For issues related to upgrading from GitHub Packages to public distribution, see the [Upgrading Guide](./updating.md).
4
+
5
+ ## Streaming SSR request hangs indefinitely
6
+
7
+ **Symptom**: Requests to streaming pages (or RSC payload endpoints) hang forever and never complete.
8
+
9
+ **Cause**: A compression middleware (`Rack::Deflater`, `Rack::Brotli`) is configured with an `:if` condition that calls `body.each` to check the response size. This destructively consumes streaming chunks from the `SizedQueue`, causing a deadlock.
10
+
11
+ **Fix**: See the "Compression Middleware Compatibility" section in the [Streaming Server Rendering guide](./streaming-server-rendering.md).
data/docs/updating.md CHANGED
@@ -198,6 +198,16 @@ export REACT_ON_RAILS_PRO_LICENSE="your-license-token-here"
198
198
 
199
199
  For complete licensing details, see [LICENSE_SETUP.md](https://github.com/shakacode/react_on_rails/blob/master/react_on_rails_pro/LICENSE_SETUP.md).
200
200
 
201
+ ### Additional Upgrade Notes
202
+
203
+ #### Upgrading to 16.4.0 or later
204
+
205
+ ##### RSC payload template overrides
206
+
207
+ React on Rails Pro now renders the built-in RSC payload template with `formats: [:text]` so Rails view annotations cannot inject HTML comments into NDJSON responses.
208
+
209
+ If your app overrides `custom_rsc_payload_template`, make sure that override resolves to a text or format-neutral template path, such as `app/views/.../rsc_payload.text.erb`. Overrides that only exist as `.html.erb` templates will raise `ActionView::MissingTemplate` when the RSC payload endpoint renders.
210
+
201
211
  ### Verify Migration
202
212
 
203
213
  #### 1. Verify Gem Installation
@@ -15,7 +15,20 @@ module ReactOnRailsPro
15
15
 
16
16
  stream_view_containing_react_components(
17
17
  template: custom_rsc_payload_template,
18
- layout: false
18
+ layout: false,
19
+ # Render as text so Rails does not inject HTML view annotation comments
20
+ # into the NDJSON stream. Custom template overrides must resolve to a
21
+ # text or format-neutral template, not `.html.erb`.
22
+ formats: [:text],
23
+ content_type: "application/x-ndjson"
24
+ )
25
+ rescue ActionView::MissingTemplate => e
26
+ raise e.exception(
27
+ "[React on Rails Pro] RSC payload templates are now rendered with format :text. " \
28
+ "If you override `custom_rsc_payload_template`, make sure the override resolves to " \
29
+ "a text or format-neutral template (for example `rsc_payload.text.erb`) instead of " \
30
+ "only `.html.erb`. See react_on_rails_pro/docs/updating.md for upgrade notes.\n\n" \
31
+ "Original error: #{e.message}"
19
32
  )
20
33
  end
21
34
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "English"
4
+
3
5
  module ReactOnRailsPro
4
6
  module Stream
5
7
  extend ActiveSupport::Concern
@@ -12,6 +14,10 @@ module ReactOnRailsPro
12
14
  #
13
15
  # @param template [String] The path to the template file to be streamed.
14
16
  # @param close_stream_at_end [Boolean] Whether to automatically close the stream after rendering (default: true).
17
+ # @param content_type [String, nil] Optional response content type. Set after rendering but before the first
18
+ # stream write, overriding any content type inferred from the template format. When using
19
+ # a non-HTML `formats:` value (for example `[:text]`), pass `content_type` too unless
20
+ # committing the format-derived MIME type is intentional.
15
21
  # @param render_options [Hash] Additional options to pass to `render_to_string`.
16
22
  #
17
23
  # components must be added to the view using the `stream_react_component` helper.
@@ -30,10 +36,13 @@ module ReactOnRailsPro
30
36
  # For more details, refer to `lib/react_on_rails/helper.rb` in the react_on_rails repository.
31
37
  #
32
38
  # @see ReactOnRails::Helper#stream_react_component
33
- def stream_view_containing_react_components(template:, close_stream_at_end: true, **render_options)
39
+ def stream_view_containing_react_components(
40
+ template:, close_stream_at_end: true, content_type: nil, **render_options
41
+ )
34
42
  require "async"
35
43
  require "async/barrier"
36
44
  require "async/limited_queue"
45
+ warn_on_non_html_formats_without_content_type(render_options[:formats], content_type)
37
46
 
38
47
  Sync do |parent_task|
39
48
  # Initialize async primitives for concurrent component streaming
@@ -48,6 +57,11 @@ module ReactOnRailsPro
48
57
  # View may contain extra newlines, chunk already contains a newline
49
58
  # Having multiple newlines between chunks causes hydration errors
50
59
  # So we strip extra newlines from the template string and add a single newline
60
+ # `formats: [:text]` causes render_to_string to set response.content_type
61
+ # to `text/plain`; override it here before the first stream write, which
62
+ # is when ActionController::Live commits headers. render_to_string itself
63
+ # never writes to response.stream, so this assignment is always safe.
64
+ response.content_type = content_type if content_type
51
65
  response.stream.write(template_string)
52
66
 
53
67
  drain_streams_concurrently(parent_task)
@@ -80,8 +94,6 @@ module ReactOnRailsPro
80
94
  # - Barrier is stopped to cancel all producer tasks, preventing wasted work
81
95
  # - No exception propagates to the controller for client disconnects
82
96
  def drain_streams_concurrently(parent_task)
83
- client_disconnected = false
84
-
85
97
  writing_task = parent_task.async do
86
98
  # Drain all remaining chunks from the queue to the response stream
87
99
  while (chunk = @main_output_queue.dequeue)
@@ -89,8 +101,13 @@ module ReactOnRailsPro
89
101
  end
90
102
  rescue IOError, Errno::EPIPE => e
91
103
  # Client disconnected - stop writing gracefully
92
- client_disconnected = true
93
104
  log_client_disconnect("writer", e)
105
+ ensure
106
+ # Cancel all producers when writer exits for ANY reason (normal completion,
107
+ # client disconnect, or unexpected error). Prevents deadlock where producers
108
+ # block on enqueue to a full queue that nobody is consuming.
109
+ # Idempotent — no-op if barrier tasks already completed.
110
+ @async_barrier.stop
94
111
  end
95
112
 
96
113
  # Wait for all component streaming tasks to complete
@@ -101,16 +118,22 @@ module ReactOnRailsPro
101
118
  raise e
102
119
  end
103
120
  ensure
104
- # Close the queue first to unblock writing_task (it may be waiting on dequeue)
105
- @main_output_queue.close
121
+ # Capture the primary exception (if any) BEFORE any cleanup that could raise.
122
+ # In an ensure block, $ERROR_INFO holds the exception currently propagating
123
+ # out of the method (nil if returning normally). We must snapshot it before
124
+ # the begin/rescue below, where $ERROR_INFO would reflect the caught exception.
125
+ primary_exception = $ERROR_INFO
106
126
 
107
- # Wait for writing_task to ensure client_disconnected flag is set
108
- # before we check it (fixes race condition where ensure runs before
109
- # writing_task's rescue block sets the flag)
110
- writing_task.wait
127
+ # Close the queue to unblock writing_task (it may be waiting on dequeue)
128
+ @main_output_queue.close
111
129
 
112
- # If client disconnected, stop all producer tasks to avoid wasted work
113
- @async_barrier.stop if client_disconnected
130
+ # Wait for writing_task to finish. Wrap in rescue to avoid masking a primary
131
+ # exception (e.g., producer error) with a secondary writing_task exception.
132
+ begin
133
+ writing_task.wait
134
+ rescue StandardError
135
+ raise unless primary_exception
136
+ end
114
137
  end
115
138
 
116
139
  def log_client_disconnect(context, exception)
@@ -120,5 +143,19 @@ module ReactOnRailsPro
120
143
  "[React on Rails Pro] Client disconnected during streaming (#{context}): #{exception.class}"
121
144
  end
122
145
  end
146
+
147
+ def warn_on_non_html_formats_without_content_type(formats, content_type)
148
+ return if content_type.present?
149
+
150
+ requested_formats = Array(formats).compact.map(&:to_sym)
151
+ return if requested_formats.empty? || requested_formats.all?(:html)
152
+
153
+ Rails.logger.warn(
154
+ "[React on Rails Pro] stream_view_containing_react_components received non-HTML formats " \
155
+ "#{requested_formats.inspect} without `content_type:`. Rails will commit the format-derived " \
156
+ "MIME type (for example `text/plain` for `:text`). Pass `content_type:` explicitly when " \
157
+ "streaming non-HTML responses."
158
+ )
159
+ end
123
160
  end
124
161
  end
@@ -7,8 +7,10 @@ module ReactOnRailsPro
7
7
  LICENSE_URL = "https://www.shakacode.com/react-on-rails-pro/"
8
8
  # TODO: Remove this legacy migration warning path after 16.5.0 stable release (target: 2026-05-31).
9
9
  LEGACY_LICENSE_FILE = "config/react_on_rails_pro_license.key"
10
+ RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS = ["Rack::Deflater"].freeze
10
11
  private_constant :LICENSE_URL
11
12
  private_constant :LEGACY_LICENSE_FILE
13
+ private_constant :RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS
12
14
 
13
15
  initializer "react_on_rails_pro.routes" do
14
16
  ActionDispatch::Routing::Mapper.include ReactOnRailsPro::Routes
@@ -20,6 +22,10 @@ module ReactOnRailsPro
20
22
  config.after_initialize { ReactOnRailsPro::Engine.log_license_status }
21
23
  end
22
24
 
25
+ initializer "react_on_rails_pro.check_rsc_streaming_middleware" do
26
+ config.after_initialize { ReactOnRailsPro::Engine.log_rsc_streaming_middleware_warning }
27
+ end
28
+
23
29
  class << self
24
30
  def log_license_status
25
31
  status = ReactOnRailsPro::LicenseValidator.license_status
@@ -40,6 +46,24 @@ module ReactOnRailsPro
40
46
  end
41
47
  end
42
48
 
49
+ def log_rsc_streaming_middleware_warning
50
+ return unless ReactOnRailsPro.configuration.enable_rsc_support
51
+ return if Rails.env.test?
52
+
53
+ middleware_names = middleware_stack_names
54
+ problematic = RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS & middleware_names
55
+ return if problematic.empty?
56
+
57
+ route_path = ReactOnRailsPro.configuration.rsc_payload_generation_url_path
58
+ Rails.logger.warn(
59
+ "[React on Rails Pro] React Server Components support is enabled and the middleware " \
60
+ "stack includes #{problematic.join(', ')}. Compression and other response-transforming " \
61
+ "middleware can interfere with ActionController::Live NDJSON streaming. If your " \
62
+ "`#{route_path}` payload route is not already exempt, consider bypassing " \
63
+ "#{problematic.join(', ')} for that endpoint if you see stalled or corrupted RSC payloads."
64
+ )
65
+ end
66
+
43
67
  private
44
68
 
45
69
  def log_valid_license
@@ -105,6 +129,40 @@ module ReactOnRailsPro
105
129
  Rails.logger.info message
106
130
  end
107
131
  end
132
+
133
+ def middleware_stack_names
134
+ middleware_stack = Rails.application&.middleware
135
+ return [] unless middleware_stack
136
+
137
+ entries =
138
+ if middleware_stack.respond_to?(:middlewares)
139
+ middleware_stack.middlewares
140
+ elsif middleware_stack.respond_to?(:to_a)
141
+ middleware_stack.to_a
142
+ else
143
+ Array(middleware_stack)
144
+ end
145
+
146
+ entries.filter_map { |entry| middleware_entry_name(entry) }.uniq
147
+ end
148
+
149
+ def middleware_entry_name(entry)
150
+ candidate =
151
+ if entry.respond_to?(:klass) && entry.klass
152
+ entry.klass
153
+ elsif entry.is_a?(Array)
154
+ entry.first
155
+ else
156
+ entry
157
+ end
158
+
159
+ case candidate
160
+ when Module
161
+ candidate.name
162
+ else
163
+ candidate.to_s.presence
164
+ end
165
+ end
108
166
  end
109
167
  end
110
168
  end
@@ -319,6 +319,16 @@ module ReactOnRailsPro
319
319
  end
320
320
 
321
321
  response = HTTPX.get(path)
322
+ error = response.error
323
+ if error
324
+ # Re-raise via rescue so Ruby sets error.cause for exception chaining.
325
+ begin
326
+ raise error
327
+ rescue StandardError
328
+ raise ReactOnRailsPro::Error, "Failed to fetch dev-server asset from #{path}: #{error}"
329
+ end
330
+ end
331
+
322
332
  response.body
323
333
  else
324
334
  Pathname.new(path)
@@ -113,7 +113,7 @@ module ReactOnRailsPro
113
113
 
114
114
  def process_response_chunks(stream_response, error_body)
115
115
  loop_response_lines(stream_response) do |chunk|
116
- if stream_response.is_a?(HTTPX::ErrorResponse) || stream_response.status >= 400
116
+ if response_has_error_status?(stream_response)
117
117
  error_body << chunk
118
118
  next
119
119
  end
@@ -123,6 +123,15 @@ module ReactOnRailsPro
123
123
  end
124
124
  end
125
125
 
126
+ def response_has_error_status?(response)
127
+ return true if response.is_a?(HTTPX::ErrorResponse)
128
+
129
+ response.status >= 400
130
+ rescue NoMethodError
131
+ # HTTPX::StreamResponse can fail to delegate #status for non-streaming errors.
132
+ true
133
+ end
134
+
126
135
  def handle_http_error(error, error_body, send_bundle)
127
136
  response = error.response
128
137
  case response.status
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ReactOnRailsPro
4
- VERSION = "16.4.0.rc.6"
4
+ VERSION = "16.4.0.rc.7"
5
5
  PROTOCOL_VERSION = "2.0.0"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react_on_rails_pro
3
3
  version: !ruby/object:Gem::Version
4
- version: 16.4.0.rc.6
4
+ version: 16.4.0.rc.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Gordon
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-05 00:00:00.000000000 Z
11
+ date: 2026-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -128,14 +128,14 @@ dependencies:
128
128
  requirements:
129
129
  - - '='
130
130
  - !ruby/object:Gem::Version
131
- version: 16.4.0.rc.6
131
+ version: 16.4.0.rc.7
132
132
  type: :runtime
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - '='
137
137
  - !ruby/object:Gem::Version
138
- version: 16.4.0.rc.6
138
+ version: 16.4.0.rc.7
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: bundler
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -223,15 +223,12 @@ files:
223
223
  - Rakefile
224
224
  - app/controllers/react_on_rails_pro/rsc_payload_controller.rb
225
225
  - app/helpers/react_on_rails_pro_helper.rb
226
- - app/views/react_on_rails_pro/rsc_payload.html.erb
226
+ - app/views/react_on_rails_pro/rsc_payload.text.erb
227
227
  - babel.config.js
228
228
  - docs/bundle-caching.md
229
229
  - docs/caching.md
230
230
  - docs/code-splitting-loadable-components.md
231
231
  - docs/configuration.md
232
- - docs/contributors-info/onboarding-customers.md
233
- - docs/contributors-info/releasing.md
234
- - docs/contributors-info/style.md
235
232
  - docs/home-pro.md
236
233
  - docs/installation.md
237
234
  - docs/js-memory-leaks.md
@@ -1,7 +0,0 @@
1
- # Creating a github OAuth Token
2
-
3
- _[Document for ShakaCode Staff](https://docs.google.com/document/d/10snzXEWgkorcai76_OxlhjQcDae_WoxRfBCdVbmcQoU/edit)_
4
-
5
- # Customer Steps
6
-
7
- See [Installation](../installation.md).
@@ -1,41 +0,0 @@
1
- # Releasing React on Rails Pro
2
-
3
- ⚠️ **This documentation is outdated.**
4
-
5
- React on Rails Pro is now released together with React on Rails using a unified release script.
6
-
7
- ## Current Release Process
8
-
9
- Please refer to the main release documentation:
10
-
11
- 👉 **[/docs/contributor-info/releasing.md](../../../docs/contributor-info/releasing.md)**
12
-
13
- Or run from the repository root:
14
-
15
- ```bash
16
- cd .. && rake -D release
17
- ```
18
-
19
- ## Quick Reference
20
-
21
- ```bash
22
- # From repository root (not from react_on_rails_pro/)
23
- cd /path/to/react_on_rails
24
-
25
- # Release with version bump
26
- rake release[17.0.0]
27
-
28
- # Dry run first (recommended)
29
- rake release[17.0.0,true]
30
-
31
- # Test with local Verdaccio
32
- rake release[17.0.0,false,verdaccio]
33
- ```
34
-
35
- This unified script releases all 5 packages together:
36
-
37
- - react-on-rails (NPM)
38
- - react-on-rails-pro (NPM)
39
- - react-on-rails-pro-node-renderer (NPM)
40
- - react_on_rails (RubyGem)
41
- - react_on_rails_pro (RubyGem)
@@ -1,41 +0,0 @@
1
- # Code Style
2
-
3
- This document describes the coding style of [ShakaCode](http://www.shakacode.com). Yes, it's opinionated, as all style guidelines should be. We shall put as little as possible into this guide and instead rely on:
4
-
5
- - Use of linters with our standard linter configuration.
6
- - References to existing style guidelines that support the linter configuration.
7
- - Anything additional goes next.
8
-
9
- ## Client Side JavaScript and React
10
-
11
- - See the [Shakacode JavaScript Style Guide](https://github.com/shakacode/style-guide-javascript)
12
-
13
- ## Style Guides to Follow
14
-
15
- Follow these style guidelines per the linter configuration. Basically, lint your code and if you have questions about the suggested fixes, look here:
16
-
17
- ### Ruby Coding Standards
18
-
19
- - [ShakaCode Ruby Coding Standards](https://github.com/shakacode/style-guide-ruby)
20
- - [Ruby Documentation](http://guides.rubyonrails.org/api_documentation_guidelines.html)
21
-
22
- ### JavaScript Coding Standards
23
-
24
- - [ShakaCode Javascript](https://github.com/shakacode/style-guide-javascript)
25
- - Use the [eslint-config-shakacode](https://github.com/shakacode/style-guide-javascript/tree/master/packages/eslint-config-shakacode) npm package with eslint.
26
- - [JSDoc](http://usejsdoc.org/)
27
-
28
- ### Git coding Standards
29
-
30
- - [Git Coding Standards](http://chlg.co/1GV2m9p)
31
-
32
- ### Sass Coding Standards
33
-
34
- - [Sass Guidelines](http://sass-guidelin.es/) by [Hugo Giraudel](http://hugogiraudel.com/)
35
- - [GitHub Front End Guidelines](http://primercss.io/guidelines/)
36
-
37
- # Git Usage
38
-
39
- - Follow a github-flow model where you branch off of master for features.
40
- - Before merging a branch to master, rebase it on top of master, by using command like `git fetch; git checkout my-branch; git rebase -i origin/master`. Clean up your commit message at this point. Be super careful to communicate with anybody else working on this branch and do not do this when others have uncommitted changes. Ideally, your merge of your feature back to master should be one nice commit.
41
- - Run hosted CI and code coverage.