Skip to content

chore: update React on Rails RC and Shakapacker 10.1.0#740

Merged
justin808 merged 9 commits into
masterfrom
jg-codex/rc-react-on-rails-16-7-0-rc-0
May 29, 2026
Merged

chore: update React on Rails RC and Shakapacker 10.1.0#740
justin808 merged 9 commits into
masterfrom
jg-codex/rc-react-on-rails-16-7-0-rc-0

Conversation

@justin808

@justin808 justin808 commented May 22, 2026

Copy link
Copy Markdown
Member

Refs shakacode/react_on_rails#3360

Summary

Updates the demo app to the current React on Rails Pro RC package set and Shakapacker 10.1.0.

  • Bumps react_on_rails_pro to 16.7.0.rc.3.
  • Bumps react-on-rails-pro and react-on-rails-pro-node-renderer to 16.7.0-rc.3.
  • Bumps the shakapacker gem and npm package to 10.1.0.
  • Updates the RSC request spec parser for the length-prefixed streaming frame protocol, including an explicit malformed-payload error for invalid hex frame lengths.
  • Updates README version targets to match the installed Ruby and npm package versions.

Version Targets

  • npm: react-on-rails@16.7.0-rc.3
  • npm: react-on-rails-pro@16.7.0-rc.3
  • npm: react-on-rails-pro-node-renderer@16.7.0-rc.3
  • gems: react_on_rails 16.7.0.rc.3, react_on_rails_pro 16.7.0.rc.3
  • npm/gem: shakapacker 10.1.0

Notes

Validation

  • bin/conductor-exec bundle check
  • bin/conductor-exec yarn install --frozen-lockfile
  • bin/conductor-exec bundle exec rubocop spec/requests/server_components_spec.rb
  • bin/conductor-exec bundle exec ruby -c spec/requests/server_components_spec.rb
  • bin/conductor-exec git diff --check

Full request spec execution requires the test PostgreSQL service; GitHub Actions will run that path after the latest push.

Summary by CodeRabbit

  • Chores
    • Bumped React on Rails Pro to 16.7.0-rc.3 and Shakapacker to 10.1.0; aligned related npm packages.
  • Documentation
    • Updated version targets in the README to reflect the new package versions.
  • Tests
    • Adjusted server-side request tests to handle length-prefixed streaming payloads and added validation for malformed chunks.

Review Change Stack


Note

Medium Risk
Release-candidate upgrades touch the SSR/RSC integration and bundler toolchain; the RSC spec change reflects a streaming protocol shift that could break payload validation if production responses diverge from the new frame format.

Overview
Aligns the demo app with React on Rails Pro 16.7.0.rc.3 and Shakapacker 10.1.0 on both Ruby (Gemfile / lock) and npm (package.json / yarn.lock), including the node renderer stack (e.g. Fastify / pino resolution changes). README version targets are updated to match.

The only behavioral test change is in spec/requests/server_components_spec.rb: RSC response parsing no longer treats each line as JSON and instead reads the length-prefixed streaming frame format (metadata JSON, tab, hex length, newline, then binary content), maps payloadType to parsed html, and adds coverage for invalid hex frame lengths.

Reviewed by Cursor Bugbot for commit 02fd3d6. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown

Walkthrough

This PR bumps react_on_rails_pro to 16.7.0.rc.3 and shakapacker to 10.1.0 in Gemfile, package.json, and README, and refactors the RSC request spec to parse application/x-ndjson as length-prefixed chunks with validation and an added error test.

Changes

Dependency Upgrades and RSC Spec Parser

Layer / File(s) Summary
Dependency version declarations
Gemfile, package.json, README.md
Bumps react_on_rails_pro / react-on-rails-pro (including node renderer) to 16.7.0-rc.3 and shakapacker to 10.1.0 in Gemfile, package.json, and README targets.
RSC length-prefixed chunk parser tests
spec/requests/server_components_spec.rb
Replaces NDJSON line-by-line parsing with length-prefixed chunk parsing helpers that strip blank frames, validate header/newline/tab, parse hex content length and truncation, extract metadata (JSON.parse), coerce html payloads, and adds a test for invalid hex length error.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped through Gemfile, JSON, and thread,
Bumped versions gently, one paw ahead.
Chunks now parsed in neat length-prefixed rows—
Tests hum a tune where the payload flows.
Shakapacker twirls; the rabbit grins, tada!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: updating React on Rails to RC 16.7.0 and Shakapacker to 10.1.0, which are the primary version bumps across Gemfile, package.json, README, and related specs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg-codex/rc-react-on-rails-16-7-0-rc-0

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

Review app commands

  • +review-app-deploy - create or redeploy this PR's review app.
  • +review-app-delete - delete this PR's review app and temporary resources.
  • +review-app-help - show setup details and workflow behavior.

For setup details, comment +review-app-help.

Comment thread package.json Outdated
Comment on lines +85 to +86
"react-on-rails-pro": "16.7.0-rc.0",
"react-on-rails-pro-node-renderer": "16.7.0-rc.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The yarn.lock file was intentionally left at 16.6.0 for these packages, which means the committed lockfile is inconsistent with package.json. Any CI step using --frozen-lockfile (e.g. yarn install --frozen-lockfile) will fail because the lockfile doesn't satisfy the new 16.7.0-rc.0 requirement. The lockfile should be regenerated by running yarn install locally and committing the result—even for RC versions—so the repo is in a coherent, reproducible state.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed by the current branch state. yarn.lock now resolves the React on Rails Pro packages at 16.7.0-rc.3, and bin/conductor-exec yarn install --frozen-lockfile passes.

Comment thread Gemfile.lock Outdated
psych (>= 4.0.0)
tsort
react_on_rails (16.6.0)
react_on_rails (16.7.0.rc.0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description notes that Gemfile.lock was manually edited (only version strings were updated, not via bundle update react_on_rails_pro). Modern Bundler versions store per-gem checksums in Gemfile.lock; if those weren't updated to match the new gem tarballs, bundle install will fail with a checksum mismatch error. Please regenerate this file with bundle update react_on_rails react_on_rails_pro and commit the result.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed by the current branch state. The lockfile has been regenerated for react_on_rails_pro 16.7.0.rc.3, and bin/conductor-exec bundle check passes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stale yarn lock blocks installs
    • Updated yarn.lock to lock react-on-rails-pro, react-on-rails-pro-node-renderer, and the nested react-on-rails dependency at 16.7.0-rc.0 so frozen installs now succeed.

Create PR

Or push these changes by commenting:

@cursor push 06cf0b472a
Preview (06cf0b472a)
diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock
+++ b/yarn.lock
@@ -8783,10 +8783,10 @@
   resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz"
   integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
 
-react-on-rails-pro-node-renderer@16.6.0:
-  version "16.6.0"
-  resolved "https://registry.npmjs.org/react-on-rails-pro-node-renderer/-/react-on-rails-pro-node-renderer-16.6.0.tgz#c13ca0f156566531d7c6e005759459b0f19472a8"
-  integrity sha512-fBZ0lKRaEe8LyVTdUsXx364zQfL6hGJuE+2qQsKo+bXm0aTVq2RtO49gzq0m7Y4xuhBTVmnPQUP0O1v1cGRzLg==
+react-on-rails-pro-node-renderer@16.7.0-rc.0:
+  version "16.7.0-rc.0"
+  resolved "https://registry.yarnpkg.com/react-on-rails-pro-node-renderer/-/react-on-rails-pro-node-renderer-16.7.0-rc.0.tgz#fcd6b23bddf81a3805f38d2f23561a6aeaa16ef2"
+  integrity sha512-kgm8iuoyT1I2DPID0AWTaSjIuVbiW39vTBBouEZ2adxzX2+YKVpuBGQgTaRedTUPcdCkuVTnSNPIgcmkY9FNmw==
   dependencies:
     "@fastify/formbody" "^7.4.0 || ^8.0.2"
     "@fastify/multipart" "^8.3.1 || ^9.0.3"
@@ -8796,12 +8796,12 @@
     lockfile "^1.0.4"
     pino "^9.0.0"
 
-react-on-rails-pro@16.6.0:
-  version "16.6.0"
-  resolved "https://registry.npmjs.org/react-on-rails-pro/-/react-on-rails-pro-16.6.0.tgz#19a5ea99d7b397dd56f14cff1f31955211b4d0a2"
-  integrity sha512-Uc8o3gdHyIETvY5J9wVUyONKOhnkw9kGJDREMHQb/IuXoB5/Vo51UK487Rcep2Z+Dzz/bEvNoF+GuZohORZ7Zw==
+react-on-rails-pro@16.7.0-rc.0:
+  version "16.7.0-rc.0"
+  resolved "https://registry.yarnpkg.com/react-on-rails-pro/-/react-on-rails-pro-16.7.0-rc.0.tgz#2b1b6f1a65dc1780e77f7c2cb3e78916bf7b9eba"
+  integrity sha512-gy7PgXJZokaDepBVnqPIzINAhxJiBYbkGN0BHu/chRjhW2L7CJO/PC5pLnWnaV6CBu8AJipM+oNBiUpEXUyA6A==
   dependencies:
-    react-on-rails "16.6.0"
+    react-on-rails "16.7.0-rc.0"
 
 react-on-rails-rsc@19.0.4:
   version "19.0.4"
@@ -8812,10 +8812,10 @@
     neo-async "^2.6.1"
     webpack-sources "^3.2.0"
 
-react-on-rails@16.6.0:
-  version "16.6.0"
-  resolved "https://registry.npmjs.org/react-on-rails/-/react-on-rails-16.6.0.tgz#da7f117fec14f420f7f6ffe6bdb34b7fc2e01b3a"
-  integrity sha512-LqLi7A0n0Tv5c3yMYlwS9s6rE82gvXNMj3sscmK2LOgIJ+mLQlOX65n0Jq5ZJ4Nsl9SRgxEOOQmcrfvDeB9F1g==
+react-on-rails@16.7.0-rc.0:
+  version "16.7.0-rc.0"
+  resolved "https://registry.yarnpkg.com/react-on-rails/-/react-on-rails-16.7.0-rc.0.tgz#4fb47203ab9f65528605233c3b6cddba69536107"
+  integrity sha512-81Lu9ToSlunduiPQm+R+C+DJ9irdPDFEN3Jvq8dyI6r5uKc9kgfsTiJkyeULjk613RBAT0apkr08GOyhaSe94A==
 
 react-proxy@^1.1.7:
   version "1.1.8"

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit e26bec4. Configure here.

Comment thread package.json Outdated
@claude

claude Bot commented May 22, 2026

Copy link
Copy Markdown

Code Review: chore: bump react_on_rails to 16.7.0.rc.0

Overview

This PR bumps react_on_rails_pro (Ruby gem) and react-on-rails-pro / react-on-rails-pro-node-renderer (npm packages) from 16.6.0 to 16.7.0.rc.0 / 16.7.0-rc.0. The changes touch Gemfile, Gemfile.lock, and package.json.


Blocking Issues

1. yarn.lock is stale (inline comment on package.json)
The PR author explicitly left yarn.lock unchanged. With Yarn v1, a --frozen-lockfile install (common in CI) will fail because the lockfile still resolves these packages to 16.6.0. Even without --frozen-lockfile, relying on Yarn to silently re-resolve at install time means the committed lockfile is not an accurate snapshot of what will actually be installed. Please run yarn install locally and commit the updated yarn.lock.

2. Gemfile.lock was manually edited, not regenerated (inline comment on Gemfile.lock)
Only the version strings in Gemfile.lock were hand-edited. Bundler (v2.2+) records per-gem checksums in the CHECKSUMS section of the lockfile; those are still pointing at the 16.6.0 tarballs. Running bundle install in strict mode will raise a checksum mismatch error. The fix is to run bundle update react_on_rails react_on_rails_pro and commit the result.


Non-blocking Observations

  • RC version suitability for a tutorial repo: End users clone this repo to learn from a working reference implementation. Pinning to an RC dependency means they may hit pre-release bugs. Consider whether a stable release is available or add a prominent note for users to be aware of this.

  • Version string format difference is correct: Ruby uses 16.7.0.rc.0 (dot-separated pre-release) and npm uses 16.7.0-rc.0 (semver pre-release identifier). This inconsistency between ecosystems is expected and not a bug.

  • react_on_rails transitive dependency: The lock file update correctly pins react_on_rails (= 16.7.0.rc.0) as a transitive dependency of react_on_rails_pro. This is the right approach since it is not declared directly in Gemfile.


Summary

The intent of the PR is correct, but the two lockfile issues need to be resolved before merge to avoid broken CI and broken bundle install for users of the repo. The fix is straightforward: run bundle update react_on_rails react_on_rails_pro && yarn install locally and commit both updated lockfiles.

justin808 added a commit to shakacode/react_on_rails that referenced this pull request May 24, 2026
…#3366) (#3368)

## Summary

Fixes [#3366](#3366) —
a 16.7.0-rc.0 regression where the RSC client manifest
(`react-client-manifest.json`) is silently dropped from the Pro Node
Renderer upload, breaking server-component rendering with `ENOENT` on
the renderer side.

- Add a side-effect import of `react-on-rails-rsc/client.browser` at the
top of `wrapServerComponentRenderer/client.tsx`. RSCWebpackPlugin scans
every parsed module's resource path for an exact match against
`require.resolve("../client.browser.js")` and only emits
`react-client-manifest.json` when it finds one. Previously the client
runtime was reachable only through a three-level transitive chain
(`wrapServerComponentRenderer/client` → `getReactServerComponent.client`
→ `react-on-rails-rsc/client.browser`); any tooling that severed a link
in that chain silently dropped the manifest and produced the misleading
`Client runtime at react-on-rails-rsc/client was not found` warning. The
direct import keeps the runtime resource in the client module graph
regardless of how downstream tooling handles transitive default imports.
- Add a structural regression test that fails if the side-effect import
is removed from either the source or the compiled lib output.
- Add a webpack-level regression test that compiles
`wrapServerComponentRenderer/client.tsx` with the old transitive
`getReactServerComponent.client` edge replaced by a stub, then asserts
`RSCWebpackPlugin` still emits `react-client-manifest.json`. This proves
the direct import itself preserves the manifest.

## Why

Issue #3366 reports that the warning + missing manifest first appears
when the consuming app upgrades to `react_on_rails_pro` /
`react-on-rails-pro` / `react-on-rails-pro-node-renderer` `16.7.0-rc.0`
from `16.6.0`, even with `react-on-rails-rsc@19.0.4` and
`shakapacker@10.0.0` unchanged. The actual chain in source/lib is intact
between both versions, and the issue does not reproduce on the in-tree
dummy app or a clean clone of the tutorial PR — but the user's
environment clearly does see `clientFileNameFound = false` from the
upstream RSC plugin. The defensive direct import removes that fragility:
even if a future transpiler/tree-shaker decides
`getReactServerComponent.client` is "unused" or rewrites the path, the
side-effect import keeps `client.browser` in the graph and the plugin
always emits the manifest.

## Evaluation

Confirmed the fix addresses the bug mechanism. The RSC plugin decides
whether to write `react-client-manifest.json` by seeing the
`react-on-rails-rsc/client.browser` module in the client compilation.
The direct side-effect import is an appropriate fix because it changes
build graph visibility without changing renderer runtime behavior.

I verified the new behavior test is meaningful by temporarily removing
the direct import: with the old transitive helper path stubbed out,
webpack emitted the expected `Client runtime at
react-on-rails-rsc/client was not found` warning and the test failed.
Restoring the import made the same test pass and emit
`react-client-manifest.json`. The Pro dummy build also emits both
`react-client-manifest.json` and `react-server-client-manifest.json`
successfully.

## Test plan

- [x] `pnpm --filter react-on-rails-pro exec jest
tests/wrapServerComponentRenderer.client.manifest.test.js --runInBand`
passes
- [x] Red check: temporarily removing `import
'react-on-rails-rsc/client.browser';` makes the new manifest test fail
with the upstream missing-client-runtime warning
- [x] `pnpm --filter react-on-rails-pro exec jest
tests/wrapServerComponentRenderer.client.manifest.test.js
tests/wrapServerComponentRenderer.client.imports.test.ts --runInBand`
passes
- [x] `pnpm --filter react-on-rails-pro exec jest
tests/createReactOnRailsPro.test.ts --runInBand` passes
- [x] `pnpm --filter react-on-rails-pro run test:non-rsc` passes: 7
suites, 58 tests
- [x] `pnpm --filter react-on-rails-pro run type-check` passes
- [x] `pnpm run lint` passes
- [x] `(cd react_on_rails && bundle exec rubocop)` passes
- [x] `(cd react_on_rails_pro && bundle exec rubocop
--ignore-parent-exclusion)` passes; RuboCop prints deprecation warnings
from dependencies but reports no offenses
- [x] `pnpm exec prettier --check
packages/react-on-rails-pro/tests/wrapServerComponentRenderer.client.manifest.test.js
packages/react-on-rails-pro/tests/fixtures/rsc-manifest/getReactServerComponent.client.stub.ts`
passes
- [x] `RAILS_ENV=test NODE_ENV=development bundle exec bin/shakapacker`
from `react_on_rails_pro/spec/dummy` passes and emits
`react-client-manifest.json` plus `react-server-client-manifest.json`
- [x] `git diff --check` passes
- [x] Pre-commit and pre-push hooks passed while committing and pushing
`3013f9d90`
- [ ] Full-repo `pnpm start format.listDifferent` is blocked by an
existing ignored `.context/plans/issue-3366-reproduction-plan.md`
formatting issue; the PR files pass direct Prettier checks
- [ ] Verify CI passes on this PR
- [ ] Verify
[shakacode/react-webpack-rails-tutorial#740](shakacode/react-webpack-rails-tutorial#740)
unblocks once a new RC build with this fix is published

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches Pro RSC client entrypoint bundling to force inclusion of the
RSC client runtime, which can affect build output and hydration behavior
if misconfigured. Added tests reduce regression risk, but changes are in
a critical RSC packaging path.
> 
> **Overview**
> Restores reliable React Server Components hydration for Pro by adding
a **top-level side-effect import** of
`react-on-rails-rsc/client.browser` in
`wrapServerComponentRenderer/client.tsx`, ensuring `RSCWebpackPlugin`
consistently emits `react-client-manifest.json` even when transitive
imports are disrupted.
> 
> Adds regression coverage: a structural test that enforces the presence
of the side-effect import (source and built `lib` when available) and a
webpack-level test that stubs `getReactServerComponent.client` to prove
manifest emission still occurs. Updates `knip.ts` to ignore dynamically
referenced test fixtures and records the fix in `CHANGELOG.md`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1bb24cc. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Restored inclusion of the client runtime so the client manifest is
emitted, preventing React Server Components hydration failures in
certain build setups.

* **Tests**
* Added regression tests to ensure the client runtime import is
preserved and the client manifest is emitted during bundling.

* **Documentation**
  * Updated CHANGELOG with the fix.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/shakacode/react_on_rails/pull/3368?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
justin808 added a commit to shakacode/react_on_rails that referenced this pull request May 24, 2026
…#3366) (#3368)

## Summary

Fixes [#3366](#3366) —
a 16.7.0-rc.0 regression where the RSC client manifest
(`react-client-manifest.json`) is silently dropped from the Pro Node
Renderer upload, breaking server-component rendering with `ENOENT` on
the renderer side.

- Add a side-effect import of `react-on-rails-rsc/client.browser` at the
top of `wrapServerComponentRenderer/client.tsx`. RSCWebpackPlugin scans
every parsed module's resource path for an exact match against
`require.resolve("../client.browser.js")` and only emits
`react-client-manifest.json` when it finds one. Previously the client
runtime was reachable only through a three-level transitive chain
(`wrapServerComponentRenderer/client` → `getReactServerComponent.client`
→ `react-on-rails-rsc/client.browser`); any tooling that severed a link
in that chain silently dropped the manifest and produced the misleading
`Client runtime at react-on-rails-rsc/client was not found` warning. The
direct import keeps the runtime resource in the client module graph
regardless of how downstream tooling handles transitive default imports.
- Add a structural regression test that fails if the side-effect import
is removed from either the source or the compiled lib output.
- Add a webpack-level regression test that compiles
`wrapServerComponentRenderer/client.tsx` with the old transitive
`getReactServerComponent.client` edge replaced by a stub, then asserts
`RSCWebpackPlugin` still emits `react-client-manifest.json`. This proves
the direct import itself preserves the manifest.

## Why

Issue #3366 reports that the warning + missing manifest first appears
when the consuming app upgrades to `react_on_rails_pro` /
`react-on-rails-pro` / `react-on-rails-pro-node-renderer` `16.7.0-rc.0`
from `16.6.0`, even with `react-on-rails-rsc@19.0.4` and
`shakapacker@10.0.0` unchanged. The actual chain in source/lib is intact
between both versions, and the issue does not reproduce on the in-tree
dummy app or a clean clone of the tutorial PR — but the user's
environment clearly does see `clientFileNameFound = false` from the
upstream RSC plugin. The defensive direct import removes that fragility:
even if a future transpiler/tree-shaker decides
`getReactServerComponent.client` is "unused" or rewrites the path, the
side-effect import keeps `client.browser` in the graph and the plugin
always emits the manifest.

## Evaluation

Confirmed the fix addresses the bug mechanism. The RSC plugin decides
whether to write `react-client-manifest.json` by seeing the
`react-on-rails-rsc/client.browser` module in the client compilation.
The direct side-effect import is an appropriate fix because it changes
build graph visibility without changing renderer runtime behavior.

I verified the new behavior test is meaningful by temporarily removing
the direct import: with the old transitive helper path stubbed out,
webpack emitted the expected `Client runtime at
react-on-rails-rsc/client was not found` warning and the test failed.
Restoring the import made the same test pass and emit
`react-client-manifest.json`. The Pro dummy build also emits both
`react-client-manifest.json` and `react-server-client-manifest.json`
successfully.

## Test plan

- [x] `pnpm --filter react-on-rails-pro exec jest
tests/wrapServerComponentRenderer.client.manifest.test.js --runInBand`
passes
- [x] Red check: temporarily removing `import
'react-on-rails-rsc/client.browser';` makes the new manifest test fail
with the upstream missing-client-runtime warning
- [x] `pnpm --filter react-on-rails-pro exec jest
tests/wrapServerComponentRenderer.client.manifest.test.js
tests/wrapServerComponentRenderer.client.imports.test.ts --runInBand`
passes
- [x] `pnpm --filter react-on-rails-pro exec jest
tests/createReactOnRailsPro.test.ts --runInBand` passes
- [x] `pnpm --filter react-on-rails-pro run test:non-rsc` passes: 7
suites, 58 tests
- [x] `pnpm --filter react-on-rails-pro run type-check` passes
- [x] `pnpm run lint` passes
- [x] `(cd react_on_rails && bundle exec rubocop)` passes
- [x] `(cd react_on_rails_pro && bundle exec rubocop
--ignore-parent-exclusion)` passes; RuboCop prints deprecation warnings
from dependencies but reports no offenses
- [x] `pnpm exec prettier --check
packages/react-on-rails-pro/tests/wrapServerComponentRenderer.client.manifest.test.js
packages/react-on-rails-pro/tests/fixtures/rsc-manifest/getReactServerComponent.client.stub.ts`
passes
- [x] `RAILS_ENV=test NODE_ENV=development bundle exec bin/shakapacker`
from `react_on_rails_pro/spec/dummy` passes and emits
`react-client-manifest.json` plus `react-server-client-manifest.json`
- [x] `git diff --check` passes
- [x] Pre-commit and pre-push hooks passed while committing and pushing
`3013f9d90`
- [ ] Full-repo `pnpm start format.listDifferent` is blocked by an
existing ignored `.context/plans/issue-3366-reproduction-plan.md`
formatting issue; the PR files pass direct Prettier checks
- [ ] Verify CI passes on this PR
- [ ] Verify
[shakacode/react-webpack-rails-tutorial#740](shakacode/react-webpack-rails-tutorial#740)
unblocks once a new RC build with this fix is published

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches Pro RSC client entrypoint bundling to force inclusion of the
RSC client runtime, which can affect build output and hydration behavior
if misconfigured. Added tests reduce regression risk, but changes are in
a critical RSC packaging path.
> 
> **Overview**
> Restores reliable React Server Components hydration for Pro by adding
a **top-level side-effect import** of
`react-on-rails-rsc/client.browser` in
`wrapServerComponentRenderer/client.tsx`, ensuring `RSCWebpackPlugin`
consistently emits `react-client-manifest.json` even when transitive
imports are disrupted.
> 
> Adds regression coverage: a structural test that enforces the presence
of the side-effect import (source and built `lib` when available) and a
webpack-level test that stubs `getReactServerComponent.client` to prove
manifest emission still occurs. Updates `knip.ts` to ignore dynamically
referenced test fixtures and records the fix in `CHANGELOG.md`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1bb24cc. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Restored inclusion of the client runtime so the client manifest is
emitted, preventing React Server Components hydration failures in
certain build setups.

* **Tests**
* Added regression tests to ensure the client runtime import is
preserved and the client manifest is emitted during bundling.

* **Documentation**
  * Updated CHANGELOG with the fix.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/shakacode/react_on_rails/pull/3368?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@justin808 justin808 changed the title chore: bump react_on_rails to 16.7.0.rc.0 chore: update React on Rails and Shakapacker RC test matrix May 24, 2026
@claude

claude Bot commented May 25, 2026

Copy link
Copy Markdown

Code Review — chore: update React on Rails and Shakapacker RC test matrix

Overview

This PR bumps react_on_rails / react_on_rails_pro from 16.6.0 → 16.7.0.rc.2 (Ruby + npm) and rewrites the RSC streaming spec to handle the new length-prefixed frame protocol. Dependency ripple-effects bring console, httpx, io-event, and most notably jwt along as updates.


Discrepancy: Shakapacker not updated

The PR description dedicates a full section to "Shakapacker 10.1.0 RC" and states:

Bumps the app to shakapacker@10.1.0-rc.2 and the shakapacker gem to 10.1.0.rc.2.

However the actual diff shows:

  • Gemfile still pins shakapacker, "10.0.0"
  • package.json has no shakapacker changes
  • yarn.lock has no shakapacker changes

This makes the PR description misleading for reviewers. If the Shakapacker update is intentionally deferred (which the description's blocking notes suggest), that section should be reworded to describe what was evaluated but not yet applied rather than framing it as a done item.


JWT major-version bump (2.10.2 → 3.2.0) — needs verification

react_on_rails_pro relaxed its constraint from jwt (~> 2.7) to jwt (>= 2.7), and Bundler resolved jwt 3.2.0. JWT 3.x is a breaking major release with changes to algorithm handling, the JWT.decode interface, and header claims. If any application-level code (or the gem itself) uses the JWT Ruby API directly, this needs explicit verification. Please confirm:

  1. The app has no direct JWT.decode / JWT.encode calls.
  2. The react_on_rails_pro 16.7.0.rc.2 gem is tested against jwt >= 3.

RSC Spec Parser — correctness notes

The new binary parser in server_components_spec.rb is generally sound — correct use of .b for binary-safe slicing, proper encoding coercion before JSON parsing, and explicit error raises with meaningful messages. A few issues worth addressing:

  1. Unhandled ArgumentError on bad hex (line 41): Integer(content_length_hex, 16) raises ArgumentError for invalid input, bypassing the explicit raise pattern used everywhere else. Wrapping it or pre-validating would give a consistent error message.

  2. byteslice length vs end-index (line 47): body.byteslice(offset, body.bytesize) works but the second argument is a length, not an end index. Using body.bytesize as the length implies "up to the full buffer size," which is correct but misleading at first read. body.byteslice(offset..) is clearer.

  3. while modifier with assignment (line 29): The one-liner body = ... while body.start_with?("\n") is valid but uncommon enough to cause a double-take. An explicit while block improves readability.

  4. Silent mutation in parse_payload_metadata (line 54): metadata.delete("payloadType") mutates the parsed hash before assigning html. The intent is clear once understood, but a brief comment would help the next reader.


Minor / Non-blocking

  • Pinning an RC gem version directly in Gemfile is appropriate for a tutorial/demo app but should be updated to a stable release before this branch could serve as a reference implementation for production setups.
  • fastify bump (^5.8.3^5.8.5) in yarn.lock is a patch update driven by the node-renderer package; no concerns.

Summary

The dependency upgrades are mechanically straightforward. The main asks before merge:

  • Clarify or fix the Shakapacker section in the PR description.
  • Verify JWT 3.x compatibility with the app and gem.
  • Minor spec-parser polish on lines 29, 41, and 47.

end

def discard_blank_frame_lines(body)
body = body.byteslice(1..) || "".b while body.start_with?("\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The while modifier with an assignment on the same line is valid Ruby but uncommon enough to cause a double-take. An explicit loop block is easier to scan:

Suggested change
body = body.byteslice(1..) || "".b while body.start_with?("\n")
def discard_blank_frame_lines(body)
while body.start_with?("\n")
body = body.byteslice(1..) || "".b
end
body
end

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this one as-is. The one-line loop is local to the spec helper and is readability/style feedback rather than a correctness issue; the selected code change here was the invalid-hex error path.

Comment thread spec/requests/server_components_spec.rb Outdated
raise "Malformed length-prefixed RSC payload: missing tab separator" unless content_length_hex

content_start = header_end + 1
content_length = Integer(content_length_hex, 16)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integer(content_length_hex, 16) raises ArgumentError on invalid hex input, which bypasses the explicit raise "Malformed …" pattern used on every other error path. Consider wrapping it for a consistent failure message:

Suggested change
content_length = Integer(content_length_hex, 16)
content_length = Integer(content_length_hex.strip, 16)

Or wrap with rescue:

      content_length = Integer(content_length_hex, 16)
rescue ArgumentError
  raise "Malformed length-prefixed RSC payload: invalid hex length '#{content_length_hex}'"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled in 39e46b9. Invalid hex content lengths now go through parse_content_length and raise the explicit malformed-payload error, with a regression example covering the zz frame length case.


[
parse_payload_metadata(metadata_json, content),
body.byteslice(content_start + content_length, body.bytesize) || "".b

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

byteslice(offset, length) takes a length as its second argument, not an end index. Passing body.bytesize works (it's large enough to capture everything remaining) but reads as if it were an end index. The endless-range form is both correct and self-documenting:

Suggested change
body.byteslice(content_start + content_length, body.bytesize) || "".b
body.byteslice(content_start + content_length..) || "".b

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as-is. The second argument is intentionally a length large enough to consume the remaining buffer, and the current behavior is correct; I kept the change focused on the malformed-hex error behavior.

def parse_payload_metadata(metadata_json, content)
metadata = JSON.parse(metadata_json.force_encoding(Encoding::UTF_8))
content.force_encoding(Encoding::UTF_8)
metadata["html"] = metadata.delete("payloadType") == "object" ? JSON.parse(content) : content

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metadata.delete("payloadType") call is doing double duty — removing the key and returning its value for the comparison. This is idiomatic Ruby but non-obvious at first read. A brief comment clarifies the intent:

Suggested change
metadata["html"] = metadata.delete("payloadType") == "object" ? JSON.parse(content) : content
# Remove payloadType from the hash and use its value to decide how to decode content.
metadata["html"] = metadata.delete("payloadType") == "object" ? JSON.parse(content) : content

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as-is. metadata.delete("payloadType") is confined to a small parser helper and the mutation is intentional; adding a comment here would mostly restate the expression.

@justin808 justin808 changed the title chore: update React on Rails and Shakapacker RC test matrix chore: update React on Rails RC and Shakapacker 10.1.0 May 28, 2026
@justin808

Copy link
Copy Markdown
Member Author

Addressed the selected review items:

  • Updated the PR title and body so the Shakapacker state reflects 10.1.0 instead of the older RC wording.
  • Verified JWT usage: this app has no direct JWT.decode / JWT.encode calls, and react_on_rails_pro 16.7.0.rc.3 allows jwt >= 2.5, < 4 with jwt 3.2.0 resolved in the lockfile.
  • Pushed 39e46b9 to handle invalid hex RSC frame lengths with an explicit malformed-payload error and a regression example.
  • Replied to the skipped/polish threads with rationale, and noted that the stale lockfile threads are already addressed by the current branch state.

Local validation run:

  • bin/conductor-exec bundle check
  • bin/conductor-exec yarn install --frozen-lockfile
  • bin/conductor-exec bundle exec rubocop spec/requests/server_components_spec.rb
  • bin/conductor-exec bundle exec ruby -c spec/requests/server_components_spec.rb
  • bin/conductor-exec git diff --check

Local request-spec execution is blocked in this workspace by the missing PostgreSQL test socket, so GitHub Actions should be the source of truth for the full request spec path.

@claude

claude Bot commented May 28, 2026

Copy link
Copy Markdown

Code Review: chore: update React on Rails RC and Shakapacker 10.1.0

Overview

Routine dependency upgrade PR that bumps react_on_rails_pro + react-on-rails-pro* to 16.7.0-rc.3, shakapacker to 10.1.0, and piggybacks Rails 8.1.2 → 8.1.3 and several other transitive dependency updates. The most substantive change is the RSC request spec parser rewritten to handle the new length-prefixed streaming frame protocol.


Dependency Changes ✅

  • Rails 8.1.2 → 8.1.3: Patch bump; ~> 8.1.2 in the Gemfile already covers it. Fine.
  • jwt 2.10.2 → 3.2.0 (major): The app has no direct JWT usage, and react_on_rails_pro 16.7.0.rc.3 widens its constraint to jwt >= 2.5, < 4, so Bundler resolves 3.2.0 cleanly. The PR notes this explicitly — good.
  • httpx / http-2 removed, async-http added: Internal transport change in react_on_rails_pro. No app-level impact.
  • pino constraint in node renderer widened from ^9.0.0 to ^9.14.0 || ^10.1.0: Matches the actual resolved version and gives the renderer room to adopt pino v10 without another lockfile bump.

Spec Refactor (spec/requests/server_components_spec.rb) ✅ with minor notes

The rewrite from line-by-line JSON parsing to binary length-prefixed frame parsing is correct in structure and handles the protocol well. A few observations:

Good:

  • .b encoding is applied at the entry point (response.body.b) and propagated correctly.
  • content&.bytesize == content_length safely catches truncated payloads without a crash.
  • Error messages are descriptive and tested (the new invalid hex length spec is a nice addition).
  • Integer(hex, 16) with a rescue ArgumentError is the idiomatic Ruby way to validate hex strings.

Minor concerns (see inline comments):

  1. force_encoding in parse_payload_metadata reinterprets bytes as UTF-8 without validating them — invalid UTF-8 in a real server response would raise Encoding::InvalidByteSequenceError from JSON.parse rather than a protocol error, making failures harder to diagnose.
  2. byteslice(offset, body.bytesize) works but is stylistically misleading — the second argument is a length, not an end index, and body.bytesize is always an overestimate. An endless range byteslice(offset..) is more expressive.

Pre-existing (not introduced here):

  • expect_valid_rsc_payload has both chunks.all? { |chunk| chunk.key?("html") } and chunks.any? { ... } assertions. Since parse_payload_metadata always injects "html", the any? check is redundant given a passing all? check — but this predates this PR.

Validation Checklist

The PR's checklist covers bundle consistency, frozen lockfile, rubocop, and syntax check. Full spec execution is deferred to GitHub Actions (reasonable — needs the PostgreSQL service). This is an acceptable validation split for a deps-only + spec-helper change.


Summary

Approve with minor notes. The dependency upgrades are clean and well-reasoned. The spec parser rewrite is correct. The two inline comments below are low-severity but worth addressing before the RC becomes GA.


def parse_payload_metadata(metadata_json, content)
metadata = JSON.parse(metadata_json.force_encoding(Encoding::UTF_8))
content.force_encoding(Encoding::UTF_8)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

force_encoding re-labels the bytes as UTF-8 without actually validating or transcoding them. If the server ever returns metadata or content with invalid UTF-8 bytes (e.g. a binary blob accidentally included in a frame), JSON.parse will raise Encoding::InvalidByteSequenceError rather than a protocol-level error, making failures harder to diagnose in CI.

For a spec helper where you control both ends this is low risk, but encode('UTF-8') (or encode('UTF-8', invalid: :replace) for maximum resilience) would catch encoding problems closer to the source:

Suggested change
content.force_encoding(Encoding::UTF_8)
metadata = JSON.parse(metadata_json.encode(Encoding::UTF_8))
content.encode(Encoding::UTF_8)


[
parse_payload_metadata(metadata_json, content),
body.byteslice(content_start + content_length, body.bytesize) || "".b

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second argument to byteslice is a length, not an end index. Passing body.bytesize as the length always overshoots (the method caps at the end of the string), which works correctly but reads like an end-index to anyone unfamiliar with the API. An endless range is more expressive and self-documenting:

Suggested change
body.byteslice(content_start + content_length, body.bytesize) || "".b
body.byteslice((content_start + content_length)..) || "".b

@justin808 justin808 marked this pull request as ready for review May 28, 2026 08:23
@greptile-apps

greptile-apps Bot commented May 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR bumps react_on_rails_pro (gem + npm) and shakapacker (gem + npm) to their 16.7.0-rc.3 / 10.1.0 versions, pulling in Rails 8.1.3, jwt 3.x, and an async-http-based networking stack in place of httpx/http-2. The spec-side change rewrites parsed_chunks to correctly speak the length-prefixed streaming frame protocol that the updated server now emits.

  • Dependency bump: react_on_rails_pro 16.6.0 → 16.7.0.rc.3, shakapacker 10.0.0 → 10.1.0; transitive updates include Rails 8.1.2 → 8.1.3, jwt 2.x → 3.x, and the swap of httpx/http-2 for async-http.
  • Spec parser rewrite (spec/requests/server_components_spec.rb): replaces line-by-line JSON.parse with a frame-aware parser that reads {metadata_json}\ {hex_length}\ {content} chunks, with explicit error messages for truncated or invalid-hex frames.
  • README version targets updated to match installed versions.

Confidence Score: 4/5

Safe to merge; the dependency updates are well-scoped and the spec parser rewrite correctly implements the length-prefixed frame protocol.

All dependency bumps are straightforward maintenance and the Gemfile.lock resolves cleanly. The spec-side parser logic handles frame boundaries, truncation, and invalid hex correctly; the one edge case (binary encoding in the error message interpolation) cannot cause test failures under normal inputs. Full integration coverage depends on the CI PostgreSQL path running green after this push.

spec/requests/server_components_spec.rb — the new frame parser; the binary-encoding edge case in the error message is minor but worth a quick look.

Important Files Changed

Filename Overview
spec/requests/server_components_spec.rb Major rewrite of the RSC payload parser from line-by-line JSON to a length-prefixed frame protocol; logic is correct but has a minor binary-encoding edge case in the error-message interpolation of content_length_hex.
Gemfile Bumps react_on_rails_pro to 16.7.0.rc.3 and shakapacker to 10.1.0; straightforward version pin change.
Gemfile.lock Reflects resolved dependency tree including Rails 8.1.3, jwt 3.2.0, async-http 0.95.1 replacing httpx/http-2; no unexpected pinning issues.
package.json Bumps react-on-rails-pro, react-on-rails-pro-node-renderer to 16.7.0-rc.3 and shakapacker to 10.1.0.
yarn.lock Lock file updated for npm packages; pino ^9.0.0 replaced by ^9.14.0
README.md Version targets updated to 16.7.0.rc.3 / 10.1.0; no content issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[response.body.b binary string] --> B[parsed_chunks loop]
    B --> C{body empty?}
    C -- yes --> D[return chunks]
    C -- no --> E[discard_blank_frame_lines]
    E --> F{body empty?}
    F -- yes --> D
    F -- no --> G[parse_length_prefixed_chunk]
    G --> H[find newline header_end]
    H --> I{found?}
    I -- no --> J[raise: missing header newline]
    I -- yes --> K[split at tab: metadata_json + hex]
    K --> L{tab found?}
    L -- no --> M[raise: missing tab separator]
    L -- yes --> N[parse_content_length Integer hex 16]
    N --> O{valid hex?}
    O -- no --> P[raise: invalid hex length]
    O -- yes --> Q[byteslice content_length bytes]
    Q --> R{bytesize matches?}
    R -- no --> S[raise: truncated content]
    R -- yes --> T[parse_payload_metadata force UTF-8]
    T --> U[return chunk + remaining body]
    U --> B
Loading

Reviews (1): Last reviewed commit: "Handle invalid RSC frame lengths" | Re-trigger Greptile

def parse_content_length(content_length_hex)
Integer(content_length_hex, 16)
rescue ArgumentError
raise "Malformed length-prefixed RSC payload: invalid hex length '#{content_length_hex}'"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 content_length_hex is ASCII-8BIT-encoded (inherited from body.b). Interpolating it into a UTF-8 string literal raises Encoding::CompatibilityError whenever the length field contains bytes above 0x7F, replacing the intended RuntimeError with an unexpected encoding crash. Forcing to UTF-8 before interpolation avoids this.

Suggested change
raise "Malformed length-prefixed RSC payload: invalid hex length '#{content_length_hex}'"
raise "Malformed length-prefixed RSC payload: invalid hex length '#{content_length_hex.force_encoding(Encoding::UTF_8)}'"

@justin808 justin808 force-pushed the jg-codex/rc-react-on-rails-16-7-0-rc-0 branch from 39e46b9 to 02fd3d6 Compare May 28, 2026 08:34
@justin808

Copy link
Copy Markdown
Member Author

+review-app-deploy

@claude

claude Bot commented May 28, 2026

Copy link
Copy Markdown

Review: chore: update React on Rails RC and Shakapacker 10.1.0

Overview

Solid dependency-bump PR. The lockfiles are consistent with the manifests. The most substantive change is in spec/requests/server_components_spec.rb, where the RSC response parser is rewritten from a line-by-line JSON reader to a length-prefixed streaming frame parser — a necessary adaptation to the new protocol in react-on-rails-pro-node-renderer 16.7.0-rc.3.

Positive notes

  • Correct use of .b (ASCII-8BIT) before slicing; prevents encoding errors on binary payloads.
  • Explicit error messages for each malformed-frame path make debugging straightforward.
  • Integer(hex, 16) with rescue ArgumentError is clean and idiomatic for strict hex parsing.
  • Exact RC version pins (16.7.0-rc.3) ensure reproducibility.

Issues

1. Transitive Rails 8.1.2 → 8.1.3 bump is undocumented
Gemfile uses ~> 8.1.2 (patch-permissive), so Bundler pulled Rails 8.1.3 into the lock. This is a correct and benign change, but it is not mentioned in the PR description or version targets. A single bullet would help reviewers confirm it's intentional.

2. Test coverage for the new frame parser
Only the invalid-hex-length error path has a new unit test. The other three raise paths (missing header newline, missing tab separator, truncated content) and multi-chunk behavior are tested only indirectly through the integration specs (which require PostgreSQL). Adding unit tests for those three paths would ensure they stay correct independently of the integration environment.

3. byteslice(offset, body.bytesize) is functional but misleading (line 47)
The second argument to byteslice(start, length) is a length, not an end index. Passing body.bytesize works (it just returns all remaining bytes) but reads like an off-by-one mistake. The endless-range form is unambiguous:

body.byteslice(content_start + content_length..) || "".b

4. metadata["html"] holds a Ruby object when payloadType == "object"
When payloadType is "object", the html key is set to JSON.parse(content) — a Hash or Array, not HTML. The key name is inherited from the existing expect_valid_rsc_payload assertion (chunk.key?("html")), so it is not a bug, but it will surprise future readers. A comment explaining the dual types would help.


[
parse_payload_metadata(metadata_json, content),
body.byteslice(content_start + content_length, body.bytesize) || "".b

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second argument to byteslice(start, length) is a length, not an end index. Passing body.bytesize is technically correct (it requests more bytes than exist, so Ruby returns what's available), but it reads like an off-by-one error. The endless-range form is unambiguous and idiomatic:

Suggested change
body.byteslice(content_start + content_length, body.bytesize) || "".b
body.byteslice(content_start + content_length..) || "".b

def parse_payload_metadata(metadata_json, content)
metadata = JSON.parse(metadata_json.force_encoding(Encoding::UTF_8))
content.force_encoding(Encoding::UTF_8)
metadata["html"] = metadata.delete("payloadType") == "object" ? JSON.parse(content) : content

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When payloadType == "object", metadata["html"] is set to a parsed Ruby Hash/Array — not HTML text. The key name html is inherited from the expect_valid_rsc_payload assertion (chunk.key?("html")), so changing it would require updating the assertion too. A brief inline comment would prevent future readers from being surprised by the dual types:

Suggested change
metadata["html"] = metadata.delete("payloadType") == "object" ? JSON.parse(content) : content
# "html" holds a String for payloadType "string" and a parsed Hash/Array for payloadType "object"
metadata["html"] = metadata.delete("payloadType") == "object" ? JSON.parse(content) : content


expect { parse_length_prefixed_chunk("#{payload}\tzz\ncontent".b) }
.to raise_error(RuntimeError, "Malformed length-prefixed RSC payload: invalid hex length 'zz'")
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good test for the hex-parsing error path. The other three raise paths in parse_length_prefixed_chunk (missing header newline, missing tab separator, truncated content) currently rely on the PostgreSQL-backed integration specs to exercise them indirectly. Consider adding unit tests for those paths here so they're validated without the DB:

it "raises a malformed payload error when the header newline is missing" do
  expect { parse_length_prefixed_chunk("no-newline-at-all".b) }
    .to raise_error(RuntimeError, "Malformed length-prefixed RSC payload: missing header newline")
end

it "raises a malformed payload error when the tab separator is missing" do
  expect { parse_length_prefixed_chunk("{}\ncontent".b) }
    .to raise_error(RuntimeError, "Malformed length-prefixed RSC payload: missing tab separator")
end

it "raises a malformed payload error when content is truncated" do
  expect { parse_length_prefixed_chunk("{}\tff\nshort".b) }
    .to raise_error(RuntimeError, "Malformed length-prefixed RSC payload: truncated content")
end

@claude

claude Bot commented May 28, 2026

Copy link
Copy Markdown

Code Review

Overview

This PR bumps React on Rails Pro and its npm siblings to 16.7.0-rc.3, Shakapacker to 10.1.0, and updates the RSC request spec to parse the new length-prefixed streaming frame protocol. The Gemfile.lock also picks up Rails 8.1.3 (allowed by the existing ~> 8.1.2 constraint) and a raft of minor gem/npm patch bumps (nokogiri, prism, rack, etc.).


Dependency changes

  • Rails 8.1.28.1.3 — Gemfile says ~> 8.1.2; the patch bump is expected. Rails 8.1.3 is a bug-fix release, no concerns.
  • rack-proxy 0.7.70.8.2 — minor version bump; no functional impact expected for this app since it only proxies webpack-dev-server in development.
  • pino ^9.0.0^9.14.0 || ^10.1.0 — the node-renderer package's peer range was widened and the old pino@9.x + pino-abstract-transport@^2 + thread-stream@^3 lockfile entries were removed. yarn.lock now resolves to pino 10.3.1. This is a transitive change inside react-on-rails-pro-node-renderer, so it's appropriate to accept here.
  • RC packages in production config — noted in the PR description; expected for a demo app tracking pre-release versions.

Spec changes (spec/requests/server_components_spec.rb)

The protocol shift from line-by-line JSON to length-prefixed binary frames is well-implemented. The parser correctly handles binary encoding, tab-delimited headers, and hex content lengths.

Issues:

  1. Dead || "".b fallback (line 29) — String#byteslice(1..) on a 1-byte string returns "", not nil, so the fallback is unreachable. See inline comment.

  2. Incomplete unit test coverage for the new parser — the new test exercises invalid hex length but the other three error paths (missing header newline, missing tab separator, truncated content) have no unit tests. These paths are exercised indirectly by the integration tests but are worth covering explicitly given the complexity added.

  3. force_encoding without validation (line 58-59) — force_encoding reinterprets bytes as UTF-8 without checking validity. For a test helper this is acceptable, but if a future frame carries binary content and JSON.parse or string comparison is called on it, you'll get an Encoding::InvalidByteSequenceError rather than a clean test failure. Low risk here since the integration tests send HTML/JSON strings, not binary blobs.


Summary

Clean, well-scoped bump PR. The only substantive nits are the dead fallback and the thin unit-test coverage for the new parser error paths. The integration tests cover the happy path; the unhappy paths are only partially tested. Approve after addressing (or explicitly accepting) those points.

end

def discard_blank_frame_lines(body)
body = body.byteslice(1..) || "".b while body.start_with?("\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The || "".b fallback is unreachable dead code. String#byteslice(1..) returns "" (not nil) when the string is exactly one byte ("\n"), and start_with?("\n") guarantees at least one byte exists before the loop body runs. Consider removing the fallback:

Suggested change
body = body.byteslice(1..) || "".b while body.start_with?("\n")
body = body.byteslice(1..) while body.start_with?("\n")

Comment on lines +72 to +77
it "raises a malformed payload error for invalid hex content lengths" do
payload = { payloadType: "string" }.to_json

expect { parse_length_prefixed_chunk("#{payload}\tzz\ncontent".b) }
.to raise_error(RuntimeError, "Malformed length-prefixed RSC payload: invalid hex length 'zz'")
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new unit test covers the invalid hex length path, but the other three error branches added in this PR have no direct tests:

  • "missing header newline" — e.g. parse_length_prefixed_chunk("no-newline-here".b)
  • "missing tab separator" — e.g. parse_length_prefixed_chunk("no-tab\n".b)
  • "truncated content" — e.g. parse_length_prefixed_chunk("{}\tff\nshort".b) (hex says 255 bytes, body only has 5)

The integration tests below exercise the happy path, but these guard clauses can silently regress. Adding three more one-liner expect { ... }.to raise_error cases here would give full coverage of the parser's error surface.

@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown

🎉 Deploy Complete!

Open Review App

Deployment successful for PR #740, commit 02fd3d6

🎮 Control Plane Console
📋 View Completed Action Build and Deploy Logs

@justin808 justin808 merged commit 6378189 into master May 29, 2026
17 checks passed
@justin808 justin808 deleted the jg-codex/rc-react-on-rails-16-7-0-rc-0 branch May 29, 2026 04:09
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown

✅ Review App Deleted

Review app for PR #740 is deleted

🎮 Control Plane Console
📋 View Workflow Logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant