Fix RSC payload JSON corruption from Rails view annotations#2535
Fix RSC payload JSON corruption from Rails view annotations#2535
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughRSC payloads are rendered as NDJSON text (formats: [:text]) and served with Content-Type Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant RailsCtrl as RscPayloadController
participant StreamHelper as stream_view_containing_react_components
participant ViewTemplate as rsc_payload.text.erb
participant ClientParser as NDJSONParser
Client->>RailsCtrl: GET /rsc_payload/:component?props=...
RailsCtrl->>StreamHelper: stream_view_containing_react_components(template: ..., content_type: "application/x-ndjson")
StreamHelper->>ViewTemplate: render template formats: [:text], layout: false
ViewTemplate-->>StreamHelper: NDJSON chunks (no HTML comments)
StreamHelper-->>RailsCtrl: write chunks (response.content_type set)
RailsCtrl-->>Client: 200 application/x-ndjson (streamed NDJSON)
Client->>ClientParser: parse NDJSON lines (valid JSON)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR fixes RSC payload JSON corruption caused by Rails' Key changes:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Client as RSC Client
participant Rails as Rails Router
participant Controller as PagesController<br/>(RSCPayloadRenderer)
participant Renderer as stream_view_containing<br/>_react_components
participant Template as rsc_payload.text.erb
Client->>Rails: GET /rsc_payload/:component_name
Rails->>Controller: rsc_payload action
Controller->>Renderer: stream_view_containing_react_components(<br/>template: "react_on_rails_pro/rsc_payload",<br/>layout: false,<br/>formats: [:text])
Note over Renderer: formats: [:text] prevents<br/>annotate_rendered_view_with_filenames<br/>from injecting HTML comments
Renderer->>Template: render rsc_payload.text.erb<br/>(text/plain format)
Template-->>Renderer: NDJSON chunks (unpolluted)
Renderer-->>Client: Streaming NDJSON<br/>Content-Type: text/plain
Last reviewed commit: 4080300 |
|
Review: Fix RSC payload JSON corruption from Rails view annotations The fix is correct and targets the root cause well. Using formats: [:text] combined with the template rename cleanly prevents Rails from applying HTML view annotations to the NDJSON stream. Content-Type side effect worth documenting The most notable side effect of formats: [:text] is that it silently changes the response Content-Type from text/html to text/plain. Neither is semantically correct for NDJSON (the proper type would be application/x-ndjson or application/octet-stream), but text/plain is a lateral move rather than a regression. Worth verifying no client-side fetch code or proxy configuration relies on the old text/html value. Explicitly setting response.headers[Content-Type] = application/x-ndjson in the concern or controller would make the intended type unambiguous and prevent future confusion. Minor issues in the spec
Overall Small, focused, and low-risk. The regression spec is a welcome addition. The content-type concern is the one item worth addressing before merging. |
|
Addressed all review threads:
Verification rerun:
|
|
Review summary posted as inline comments. Four items flagged: (1) Content-Type header set before render_to_string may be overridden by Rails format negotiation - pass content_type as a render option instead. (2) custom_rsc_payload_template override point needs a comment warning that custom templates must use .text.erb not .html.erb. (3) The spec does not assert HTML annotation comments are absent from the body, only that JSON lines parse. (4) ActionView::Base.annotate_rendered_view_with_filenames is a global class-level flag; thread-safe under sequential runs but fragile if parallel execution is ever enabled. |
Review SummaryThe fix is correct and well-targeted — forcing Two things worth addressing before merge: 1.
2. Missing baseline test (inline comment on spec line 16) The new spec only exercises the annotated-view scenario. Adding a second example without the annotation override would confirm that the 3. Breaking change documentation (minor) The CHANGELOG correctly calls out that users overriding Overall the implementation is sound — these are improvements rather than blockers. |
|
Checked against current |
* origin/master: Fix private_output_path not configured on fresh Shakapacker installs (#2289) Bump the npm-security group across 1 directory with 3 updates (#2387) docs: use https links (#2518) Consolidate changelog to keep only rc6 prerelease (#2533) Fix CSS module class name divergence with rspack SSR (#2489) Bump version to 16.4.0.rc.6 # Conflicts: # CHANGELOG.md
Code ReviewThe fix is correct and well-targeted. Using Ordering of Content-Type header assignment
Test spec — class-level mutation The Test assertion breadth
CHANGELOG / migration note The CHANGELOG entry for custom |
| end | ||
|
|
||
| def warn_on_non_html_formats_without_content_type(formats, content_type) | ||
| return if content_type.present? |
There was a problem hiding this comment.
present? treats empty string as absent
content_type.present? returns false for "" (an empty string). If a caller inadvertently passes content_type: "", this method will still emit the warning (because "".present? is false), which is confusing — the caller explicitly set content_type, just to an empty value.
content_type.nil? (or !content_type) would more precisely express "was no value passed at all":
| return if content_type.present? | |
| return unless content_type.nil? |
| ) | ||
| end | ||
|
|
||
| it "logs a startup warning when RSC support is enabled with Rack::Deflater in development" do |
There was a problem hiding this comment.
Missing test case: warning fires in production too
log_rsc_streaming_middleware_warning only skips in test env, meaning it is also active in production. The suite covers development (warning fires) and test (warning suppressed), but there is no case for production. Adding a case would lock in the intended behaviour and guard against accidental early-return logic being added for prod:
it "logs a startup warning when RSC support is enabled with Rack::Deflater in production" do
allow(Rails).to receive_messages(
application: build_application(Rack::Deflater),
env: ActiveSupport::StringInquirer.new("production")
)
allow(ReactOnRailsPro).to receive(:configuration)
.and_return(build_rsc_configuration(enable_rsc_support: true))
expect(mock_logger).to receive(:warn).with(a_string_including("Rack::Deflater"))
described_class.log_rsc_streaming_middleware_warning
end| LICENSE_URL = "https://www.shakacode.com/react-on-rails-pro/" | ||
| # TODO: Remove this legacy migration warning path after 16.5.0 stable release (target: 2026-05-31). | ||
| LEGACY_LICENSE_FILE = "config/react_on_rails_pro_license.key" | ||
| RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS = ["Rack::Deflater"].freeze |
There was a problem hiding this comment.
Consider noting other response-transforming middleware
RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS currently only lists Rack::Deflater. Third-party compression middleware (Rack::Brotli, Rack::Gzip, etc.) have the same streaming-corruption risk but won't be detected. A comment here would help maintainers know when to extend this list:
| RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS = ["Rack::Deflater"].freeze | |
| RSC_STREAMING_MIDDLEWARE_WARNING_TARGETS = [ | |
| "Rack::Deflater" | |
| # Add other response-transforming middleware here if they are found to | |
| # interfere with ActionController::Live NDJSON streaming (e.g. Rack::Brotli). | |
| ].freeze |
Summary
formats: [:text]so Rails does not apply HTML view annotationsrsc_payload.text.erbActionView::Base.annotate_rendered_view_with_filenamesand verifies NDJSON lines remain parseableFixes #2520
Test Plan
bundle exec rubocop react_on_rails_pro/lib/react_on_rails_pro/concerns/rsc_payload_renderer.rb react_on_rails_pro/spec/dummy/spec/requests/rsc_payload_spec.rbcd react_on_rails_pro/spec/dummy && bundle installcd react_on_rails_pro/spec/dummy && pnpm run node-renderer(separate terminal)cd react_on_rails_pro/spec/dummy && bundle exec rspec spec/requests/rsc_payload_spec.rbNote
Low Risk
Small change to response rendering options plus a regression test; low risk aside from potential clients expecting a different negotiated format.
Overview
For the
rsc_payloadstreaming endpoint, forces the renderformatsto[:text]so Rails doesn’t inject HTML view-annotation comments that can corrupt the NDJSON payload.Adds a request spec that enables
ActionView::Base.annotate_rendered_view_with_filenamesand asserts the response lines remain JSON-parseable and contain expectedhtmlchunks.Written by Cursor Bugbot for commit fe6374b. Configure here.
Summary by CodeRabbit
Bug Fixes
Tests
Documentation