Skip to content

Fix upload-assets endpoint duplicating bundles across directories#2768

Merged
AbanoubGhadban merged 7 commits intomainfrom
fix/2766-upload-assets-bundle-filtering
Mar 26, 2026
Merged

Fix upload-assets endpoint duplicating bundles across directories#2768
AbanoubGhadban merged 7 commits intomainfrom
fix/2766-upload-assets-bundle-filtering

Conversation

@AbanoubGhadban
Copy link
Copy Markdown
Collaborator

@AbanoubGhadban AbanoubGhadban commented Mar 19, 2026

Summary

  • Fixes the /upload-assets endpoint blindly copying all uploaded files into every target bundle directory, which duplicated bundle JS files when RSC support is enabled
  • Extracts a shared extractBundlesAndAssets() helper used by both the render and /upload-assets endpoints, DRYing up form body parsing
  • Rewrites /upload-assets to reuse handleNewBundlesProvided() from the render path, ensuring each bundle goes only to its own directory
  • Removes the now-redundant targetBundles form field from /upload-assets — target directories are derived from bundle_<hash> form keys
  • Net reduction of 27 lines

Test plan

  • All 18 worker.test.ts tests pass (including updated /upload-assets tests)
  • All 5 uploadRaceCondition.test.ts tests pass (concurrent isolation, serialization, cross-endpoint locking)
  • All 13 handleRenderRequest.test.ts tests pass (no regressions in render path)
  • ESLint passes on all modified files
  • CI passes

Closes #2766

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed bundle duplication during remote node-renderer uploads with RSC enabled: each bundle now goes to its own directory while shared assets remain distributed.
  • Chores

    • Uploads now provide bundles via per-bundle form fields (e.g., bundle_) instead of a separate target list.
    • Protocol version bumped to 2.1.0.
  • Tests

    • Tests updated to use per-bundle form fields for uploads and render requests.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 19, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Parses multipart form keys into per-bundle bundle_<hash> entries and shared assets; /upload-assets now requires bundle fields and uses handleNewBundlesProvided to place each bundle only in its matching directory while distributing shared assets to all targets; Ruby request builder, tests, and protocol version updated.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added Pro-only fix: prevent duplicating bundle JS across all bundle directories; place each bundle in its matching directory and distribute shared assets to all target locations.
Node renderer worker
packages/react-on-rails-pro-node-renderer/src/worker.ts
Added extractBundlesAndAssets() to classify bundle_<hash> fields and assets; /upload-assets now validates presence of bundle_<...> fields and delegates to handleNewBundlesProvided instead of per-bundle copy/lock loops.
Render request handling
packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts
Exported handleNewBundlesProvided; renamed parameter to requestContext; moved asset copy to occur unconditionally after move logic so shared assets are copied even if a bundle move was skipped.
Tests
packages/react-on-rails-pro-node-renderer/tests/...
packages/react-on-rails-pro-node-renderer/tests/uploadRaceCondition.test.ts, packages/react-on-rails-pro-node-renderer/tests/worker.test.ts
Updated test request payloads to use dynamic bundle_<hash> form fields (streamed fixtures) instead of targetBundles arrays; adjusted imports for secondary fixture; no assertion changes.
Ruby request builder
react_on_rails_pro/lib/react_on_rails_pro/request.rb
Stopped sending targetBundles field; uses form_with_assets_and_bundle to include bundle_<hash> file fields (keeps local existence checks).
Protocol version
packages/react-on-rails-pro-node-renderer/package.json, react_on_rails_pro/lib/react_on_rails_pro/version.rb
Bumped protocol version from 2.0.02.1.0 in Node package.json and Ruby constant.

Sequence Diagram(s)

sequenceDiagram
  participant Ruby as Ruby (rake/app)
  participant Client as HTTP Client
  participant Worker as Node Renderer Worker
  participant FS as Filesystem

  Ruby->>Client: form_with_assets_and_bundle (fields: bundle_<hash>=file, assets=files)
  Client->>Worker: POST /upload-assets (multipart)
  Worker->>Worker: extractBundlesAndAssets(req.body)
  alt providedNewBundles exist
    Worker->>Worker: handleNewBundlesProvided(taskDesc, providedNewBundles, assetsToCopy)
    Worker->>FS: place each bundle into its matching bundle directory
    Worker->>FS: copy shared assets into all target directories
  else no bundles found
    Worker-->>Client: 4xx validation error
  end
  Worker-->>Client: ResponseResult (success/failure)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped through multipart fields with cheer,
Found each bundle its home, kept assets near.
No more copies scattered wide,
Neat directories side by side.
Hooray — tidy bundles, carrots for all here! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: fixing the /upload-assets endpoint that was duplicating bundles across directories.
Linked Issues check ✅ Passed The PR fully addresses issue #2766 by eliminating bundle duplication, filtering bundles per target directory, and aligning /upload-assets with the render endpoint's bundle handling.
Out of Scope Changes check ✅ Passed All changes align with issue #2766 objectives: extracting shared parsing logic, refactoring endpoints to prevent bundle duplication, removing redundant targetBundles field, and updating protocol version.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/2766-upload-assets-bundle-filtering

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.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 19, 2026

Review: Fix upload-assets endpoint duplicating bundles across directories

Overall this is a solid fix — the root cause is correctly identified and the DRY refactor is clean. A few concerns worth addressing before merge:

Breaking protocol change without a version bump

The /upload-assets endpoint now rejects any client that sends the old targetBundles field (returning an error unless a bundle_<hash> key is also present). This is a hard breaking change between the Ruby gem and the node renderer. Deploying them in mismatched order will break asset uploads silently or noisily depending on which side is updated first.

The codebase has a protocolVersion mechanism for exactly this situation. This change should either increment protocolVersion and gate the new behaviour on the Ruby side, or accept both the old (targetBundles) and new (bundle_<hash>) formats during a transition window.

handleNewBundlesProvided — semantic mismatch on first parameter

The first parameter is typed/named renderingRequest: string. For the render endpoint that is a JSON blob; the upload endpoint passes taskDescription (a human-readable string) instead. Error messages and logs that surface this value will show the task description rather than a request payload. Functional, but confusing — consider renaming the parameter to something neutral like context.

CHANGELOG references wrong PR number

The entry says "PR 2767" but this PR is 2768.

Redundant RSC bundle existence check in request.rb

The early guard in upload_assets that raises when the RSC bundle is missing is now redundant: form_with_assets_and_bundle -> populate_form_with_bundle_and_assets(check_bundle: true) -> add_bundle_to_form already raises ReactOnRailsPro::Error, "Bundle not found #{bundle_path}". The early guard provides a friendlier message (fine to keep), but deserves a comment explaining the duplication.

Minor: Body type annotation is misleading

The handler types the body as Record<string, Asset> but scalar fields (gemVersion, protocolVersion, railsEnv, etc.) are also present. The runtime isAsset() guard handles this correctly, but Record<string, unknown> would be a more accurate type.

Comment thread CHANGELOG.md

### [Unreleased]

#### Fixed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The PR link says PR 2767 but this PR is 2768. Please update the CHANGELOG entry.

Suggested change
#### Fixed
- **[Pro] Fixed bundle duplication in remote node renderer asset uploads**: When RSC support is enabled, running `rake react_on_rails_pro:copy_assets_to_remote_vm_renderer` no longer duplicates bundle JS files across bundle directories. Previously, both the server bundle and RSC bundle were copied into every target directory; now each bundle is placed only in its own directory while shared assets (manifests, stats) are correctly distributed to all. [PR 2768](https://github.com/shakacode/react_on_rails/pull/2768) by [AbanoubGhadban](https://github.com/AbanoubGhadban). Fixes [Issue 2766](https://github.com/shakacode/react_on_rails/issues/2766).


// There can be additional files that might be required at the runtime.
// Since the remote renderer doesn't contain any assets, they must be uploaded manually.
// Bundle files use the form key convention "bundle_<hash>" and are placed in
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The Body type Record<string, Asset> is inaccurate — scalar fields like gemVersion, protocolVersion, railsEnv, and password are also present in the body. The runtime isAsset() filtering handles this correctly, but the type annotation is misleading.

Suggested change
// Bundle files use the form key convention "bundle_<hash>" and are placed in
Body: Record<string, unknown>;

const firstFailure = results.find((r): r is PromiseRejectedResult => r.status === 'rejected');
if (firstFailure) {
throw firstFailure.reason;
try {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

handleNewBundlesProvided's first parameter is named renderingRequest (a JSON blob in the render path). Here taskDescription is passed instead — a human-readable string. This is functional, but any error messages or log lines in handleNewBundlesProvided that reference "renderingRequest" will surface this task description string, which can be confusing when debugging. Consider renaming the parameter to something context-neutral (e.g. context or requestContext) in handleRenderRequest.ts.

target_bundles = [pool.server_bundle_hash]

# Add RSC bundle if enabled
if ReactOnRailsPro.configuration.enable_rsc_support
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This existence check is now redundant: form_with_assets_and_bundlepopulate_form_with_bundle_and_assets(check_bundle: true)add_bundle_to_form already raises ReactOnRailsPro::Error, "Bundle not found #{bundle_path}" when the file is missing. The early check provides a friendlier error message (worth keeping), but a brief comment would help future readers understand why the check is intentionally duplicated.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 19, 2026

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.63 KB (0%)
react-on-rails/client bundled (gzip) (time) 62.63 KB (0%)
react-on-rails/client bundled (brotli) 53.7 KB (0%)
react-on-rails/client bundled (brotli) (time) 53.7 KB (0%)
react-on-rails-pro/client bundled (gzip) 63.65 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 63.65 KB (0%)
react-on-rails-pro/client bundled (brotli) 54.67 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 54.67 KB (0%)
registerServerComponent/client bundled (gzip) 127.46 KB (0%)
registerServerComponent/client bundled (gzip) (time) 127.46 KB (0%)
registerServerComponent/client bundled (brotli) 61.65 KB (0%)
registerServerComponent/client bundled (brotli) (time) 61.65 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 122.02 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 122.02 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.77 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.77 KB (0%)

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50290afe01

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

perform_request("/upload-assets", form: form)
# The node renderer derives target directories from the bundle_<hash>
# form keys, so no separate targetBundles field is needed.
perform_request("/upload-assets", form: form_with_assets_and_bundle)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep targetBundles in upload_assets for protocol 2.0

Dropping targetBundles here creates a wire-level incompatibility with older node-renderer versions that still advertise protocol 2.0.0 and reject /upload-assets without that field. Because production only warns on gem/package version mismatch (it does not block requests), mixed-version rollouts are a supported scenario; if the gem is upgraded before the remote renderer, upload_assets now fails with 400 and the deployment task cannot sync assets. Please keep sending targetBundles (at least as a compatibility fallback) or bump protocol version if this is an intentional breaking change.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 19, 2026

Greptile Summary

This PR fixes a bundle duplication bug in the /upload-assets endpoint of the remote node renderer: when RSC support was enabled, both the server bundle and the RSC bundle were being copied into every target bundle directory instead of each going to its own directory.

Key changes:

  • New extractBundlesAndAssets() helper (worker.ts): centralises the logic for classifying multipart form fields as bundles (bundle_<hash> keys) vs shared assets. Used by both the render endpoint and /upload-assets, eliminating the previous code duplication.
  • /upload-assets rewrite (worker.ts): removed the targetBundles field dependency; now derives target directories from bundle_<hash> form keys and delegates to the existing handleNewBundlesProvided() from the render path — ensuring each bundle is moved to its own directory while shared assets (manifests, stats) are correctly copied to all.
  • handleNewBundlesProvided exported (handleRenderRequest.ts): minimal change to allow reuse from worker.ts.
  • Ruby client updated (request.rb): targetBundles field removed from the /upload-assets POST; the RSC bundle existence pre-check is correctly retained.
  • Tests updated: all /upload-assets test fixtures switched from targetBundles arrays to bundle_<hash> file fields; a new multi-bundle upload test and an updated race-condition suite cover the fixed behaviour.
  • One minor issue: the CHANGELOG entry links to PR #2767 instead of the correct #2768.

Confidence Score: 4/5

  • Safe to merge after fixing the CHANGELOG PR link typo; the core logic change is well-tested and correctly scoped.
  • The bug fix is well-reasoned and the implementation correctly reuses the existing handleNewBundlesProvided + per-bundle locking machinery. All three test suites cover the new behaviour including concurrent isolation and same-bundle serialisation. The only issue found is a wrong PR number in the CHANGELOG and a minor replace vs slice style point in extractBundlesAndAssets; neither affects runtime correctness.
  • CHANGELOG.md (wrong PR link), packages/react-on-rails-pro-node-renderer/src/worker.ts line 119 (replace vs slice)

Important Files Changed

Filename Overview
packages/react-on-rails-pro-node-renderer/src/worker.ts Core fix: adds extractBundlesAndAssets() helper and rewrites /upload-assets to use handleNewBundlesProvided(), correctly routing each bundle to its own directory. Minor: replace('bundle_', '') should be slice('bundle_'.length) to be strictly correct.
packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts Single change: exports handleNewBundlesProvided so it can be reused from worker.ts. No logic changes; existing lock and move/copy behaviour is unchanged.
react_on_rails_pro/lib/react_on_rails_pro/request.rb Removes the now-redundant targetBundles field from the /upload-assets form. The bundle-to-directory mapping is now fully driven by bundle_<hash> keys already present in form_with_assets_and_bundle. RSC bundle existence check is correctly retained.
packages/react-on-rails-pro-node-renderer/tests/worker.test.ts Tests updated to the new protocol: targetBundles array replaced with bundle_<hash> file fields. Multi-bundle test correctly sends two different fixture bundles and verifies assets appear in both directories.
packages/react-on-rails-pro-node-renderer/tests/uploadRaceCondition.test.ts Race condition tests updated to the new protocol; imports getFixtureSecondaryBundle (which exists in fixtures). Tests verify both isolation of different-bundle requests and serialisation of same-bundle requests.
CHANGELOG.md Correct entry added for the fix, but PR link references #2767 instead of #2768 — a copy-paste error that will produce a broken link.

Sequence Diagram

sequenceDiagram
    participant Ruby as Ruby Client
    participant Node as worker.ts
    participant HNBP as handleNewBundlesProvided
    participant FS as File System

    Ruby->>Node: "POST /upload-assets (bundle_hashA, bundle_hashB, assets)"
    Node->>Node: "extractBundlesAndAssets() splits bundles from assets"
    Node->>HNBP: "handleNewBundlesProvided(bundles=[hashA,hashB], assets)"

    par hashA bundle
        HNBP->>FS: "lock(hashA) → move serverBundle → bundleDir/hashA/"
        HNBP->>FS: "copy shared assets → bundleDir/hashA/"
        HNBP->>FS: "unlock(hashA)"
    and hashB bundle
        HNBP->>FS: "lock(hashB) → move rscBundle → bundleDir/hashB/"
        HNBP->>FS: "copy shared assets → bundleDir/hashB/"
        HNBP->>FS: "unlock(hashB)"
    end

    HNBP-->>Node: "undefined (success)"
    Node-->>Ruby: "200 OK"
Loading

Comments Outside Diff (1)

  1. packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts, line 153-157 (link)

    P2 Misleading parameter name for non-render callers

    The parameter renderingRequest is now used by two different callers:

    • The render endpoint passes the actual rendering request JSON string.
    • The new /upload-assets endpoint (in worker.ts) passes taskDescription — a human-readable log label.

    Since the parameter is only forwarded to formatExceptionMessage for error context, the mismatch is functionally harmless. However, for future maintainers, the parameter name implies the first argument must be a rendering request. A more neutral name such as contextLabel or taskDescription would better reflect the dual-use contract, e.g.:

    export async function handleNewBundlesProvided(
      contextLabel: string,
      providedNewBundles: ProvidedNewBundle[],
      assetsToCopy: Asset[] | null | undefined,
    ): Promise<ResponseResult | undefined>
    

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Last reviewed commit: "Fix upload-assets en..."

Comment thread CHANGELOG.md

### [Unreleased]

#### Fixed
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 Incorrect PR number in changelog entry

The changelog entry links to PR 2767, but this PR is #2768. This will create a broken link in the published changelog.

Suggested change
#### Fixed
- **[Pro] Fixed bundle duplication in remote node renderer asset uploads**: When RSC support is enabled, running `rake react_on_rails_pro:copy_assets_to_remote_vm_renderer` no longer duplicates bundle JS files across bundle directories. Previously, both the server bundle and RSC bundle were copied into every target directory; now each bundle is placed only in its own directory while shared assets (manifests, stats) are correctly distributed to all. [PR 2768](https://github.com/shakacode/react_on_rails/pull/2768) by [AbanoubGhadban](https://github.com/AbanoubGhadban). Fixes [Issue 2766](https://github.com/shakacode/react_on_rails/issues/2766).

providedNewBundles.push({ timestamp: primaryBundleTimestamp, bundle: value });
} else if (key.startsWith('bundle_')) {
assertAsset(value, key);
providedNewBundles.push({ timestamp: key.replace('bundle_', ''), bundle: value });
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 replace only removes first prefix occurrence

String.prototype.replace(string, string) replaces only the first occurrence of the search string. For a key like bundle_bundle_abc, the derived timestamp would be bundle_abc instead of abc, which would cause the bundle to be placed in the wrong directory.

A more defensive approach is to strip the known prefix with slice (which is also slightly more efficient since no regex or search is needed):

Suggested change
providedNewBundles.push({ timestamp: key.replace('bundle_', ''), bundle: value });
providedNewBundles.push({ timestamp: key.slice('bundle_'.length), bundle: value });

In practice the Ruby client strictly follows the bundle_<hash> format, so this won't trigger today, but the intent is clearer with slice.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/react-on-rails-pro-node-renderer/tests/worker.test.ts (1)

345-370: Assert the duplication regression directly.

This proves the shared assets were copied, but it never checks that each bundle directory omits the other bundle's JS file—the specific bug this PR is fixing. A couple of negative assertions here would make the regression coverage much tighter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-on-rails-pro-node-renderer/tests/worker.test.ts` around lines
345 - 370, Add negative assertions to the "post /upload-assets with multiple
bundles and assets" test to ensure each bundle directory does NOT contain the
other bundle's JS file: after the existing positive expects for assetPath(...)
and assetPathOther(...), assert that assetPath(testName, bundleHashOther) and
assetPathOther(testName, bundleHashOther) are false for the first bundle's
checks and that assetPath(testName, bundleHash) and assetPathOther(testName,
bundleHash) are false for the second bundle's checks (use the same assetPath and
assetPathOther helpers and bundleHash / bundleHashOther variables).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 29: The changelog entry currently references "[PR 2767]" but the comment
indicates this should point to PR 2768; update the PR link text and URL in the
CHANGELOG.md entry (the "[PR 2767](...)" token) to "[PR
2768](https://github.com/shakacode/react_on_rails/pull/2768)" so the entry links
to the correct pull request while leaving the rest of the paragraph (issue and
author references) unchanged.

In `@react_on_rails_pro/lib/react_on_rails_pro/request.rb`:
- Around line 62-64: The upload-assets wire format changed to derive bundle
targets from bundle_<hash> form keys (see perform_request("/upload-assets",
form: form_with_assets_and_bundle)), so bump the shared protocol version string
used by the gem and the node renderer (currently "2.0.0") to a new version (e.g.
"2.1.0") in both places where protocolVersion is declared so the worker can
detect the format change; update the Ruby protocol constant and the package.json
protocolVersion field to the new value and ensure any code that checks
protocolVersion is comparing against the new version.

---

Nitpick comments:
In `@packages/react-on-rails-pro-node-renderer/tests/worker.test.ts`:
- Around line 345-370: Add negative assertions to the "post /upload-assets with
multiple bundles and assets" test to ensure each bundle directory does NOT
contain the other bundle's JS file: after the existing positive expects for
assetPath(...) and assetPathOther(...), assert that assetPath(testName,
bundleHashOther) and assetPathOther(testName, bundleHashOther) are false for the
first bundle's checks and that assetPath(testName, bundleHash) and
assetPathOther(testName, bundleHash) are false for the second bundle's checks
(use the same assetPath and assetPathOther helpers and bundleHash /
bundleHashOther variables).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a6a8f93d-b10d-476b-b716-9402d9fcf3c9

📥 Commits

Reviewing files that changed from the base of the PR and between 5912b3b and 50290af.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • packages/react-on-rails-pro-node-renderer/src/worker.ts
  • packages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.ts
  • packages/react-on-rails-pro-node-renderer/tests/uploadRaceCondition.test.ts
  • packages/react-on-rails-pro-node-renderer/tests/worker.test.ts
  • react_on_rails_pro/lib/react_on_rails_pro/request.rb

Comment thread CHANGELOG.md Outdated
Comment on lines +62 to +64
# The node renderer derives target directories from the bundle_<hash>
# form keys, so no separate targetBundles field is needed.
perform_request("/upload-assets", form: form_with_assets_and_bundle)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files in this PR that changed the /upload-assets contract:"
git diff --name-only origin/main...HEAD -- \
  react_on_rails_pro/lib/react_on_rails_pro/request.rb \
  packages/react-on-rails-pro-node-renderer/src/worker.ts \
  packages/react-on-rails-pro-node-renderer/package.json

echo
echo "Current protocolVersion declarations/usages:"
rg -n 'protocolVersion' packages/react-on-rails-pro-node-renderer react_on_rails_pro

echo
echo "Protocol manifest diff in this PR:"
git diff --unified=0 origin/main...HEAD -- packages/react-on-rails-pro-node-renderer/package.json

Repository: shakacode/react_on_rails

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check git status and available refs
echo "=== Git Status ==="
git status || echo "Not a git repo or status failed"

echo
echo "=== Available refs ==="
git branch -a 2>/dev/null || echo "No branches found"

echo
echo "=== Locating protocolVersion declarations ==="
find . -type f \( -name "*.json" -o -name "*.ts" -o -name "*.js" -o -name "*.rb" \) -exec grep -l 'protocolVersion' {} \; 2>/dev/null | head -20

echo
echo "=== protocolVersion in package.json files ==="
find packages -name "package.json" -exec grep -H 'protocolVersion' {} \; 2>/dev/null

echo
echo "=== protocolVersion in React on Rails Pro files ==="
rg -n 'protocolVersion' react_on_rails_pro 2>/dev/null || echo "No matches in react_on_rails_pro"

Repository: shakacode/react_on_rails

Length of output: 1561


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Gem PROTOCOL_VERSION constant ==="
grep -n 'PROTOCOL_VERSION' react_on_rails_pro/lib/react_on_rails_pro/utils.rb | head -5

echo
echo "=== Full context around PROTOCOL_VERSION in utils.rb ==="
grep -B 2 -A 2 'PROTOCOL_VERSION' react_on_rails_pro/lib/react_on_rails_pro/utils.rb | head -20

echo
echo "=== request.rb lines 50-75 (context around 62-64) ==="
sed -n '50,75p' react_on_rails_pro/lib/react_on_rails_pro/request.rb

echo
echo "=== Check for targetBundles in request.rb ==="
grep -n 'targetBundles' react_on_rails_pro/lib/react_on_rails_pro/request.rb || echo "No targetBundles found"

echo
echo "=== Git diff: compare HEAD with origin/main for request.rb ==="
git diff origin/main HEAD -- react_on_rails_pro/lib/react_on_rails_pro/request.rb | head -100

Repository: shakacode/react_on_rails

Length of output: 3634


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find PROTOCOL_VERSION constant definition ==="
find react_on_rails_pro -type f -name "*.rb" -exec grep -n "PROTOCOL_VERSION\s*=" {} + 

echo
echo "=== Check for version file or constants ==="
ls -la react_on_rails_pro/lib/react_on_rails_pro/*.rb | head -20

echo
echo "=== Check config or constants file ==="
cat react_on_rails_pro/lib/react_on_rails_pro/version.rb 2>/dev/null || echo "No version.rb"

echo
echo "=== worker.ts handling of bundle_<hash> fields ==="
grep -n -A 5 'bundle_' packages/react-on-rails-pro-node-renderer/src/worker.ts | head -50

echo
echo "=== Compare versions: package.json vs origin/main ==="
git show origin/main:packages/react-on-rails-pro-node-renderer/package.json | grep -i version | head -5

echo
echo "=== Current package.json version ==="
cat packages/react-on-rails-pro-node-renderer/package.json | grep -i version | head -5

Repository: shakacode/react_on_rails

Length of output: 3788


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Git diff for version.rb (gem PROTOCOL_VERSION) ==="
git diff origin/main HEAD -- react_on_rails_pro/lib/react_on_rails_pro/version.rb

echo
echo "=== Git diff for node-renderer package.json (protocolVersion) ==="
git diff origin/main HEAD -- packages/react-on-rails-pro-node-renderer/package.json | head -50

echo
echo "=== Check when these protocol versions were introduced ==="
echo "Gem PROTOCOL_VERSION history:"
git log --oneline -p origin/main -- react_on_rails_pro/lib/react_on_rails_pro/version.rb | grep -A 2 "PROTOCOL_VERSION" | head -20

echo
echo "=== Check if targetBundles was removed on origin/main ==="
git show origin/main:react_on_rails_pro/lib/react_on_rails_pro/request.rb | grep -n "targetBundles" | head -5

Repository: shakacode/react_on_rails

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Origin/main: gem PROTOCOL_VERSION ==="
git show origin/main:react_on_rails_pro/lib/react_on_rails_pro/version.rb 2>/dev/null | grep PROTOCOL_VERSION

echo
echo "=== Origin/main: node-renderer protocolVersion ==="
git show origin/main:packages/react-on-rails-pro-node-renderer/package.json 2>/dev/null | grep protocolVersion

echo
echo "=== Current HEAD: gem PROTOCOL_VERSION ==="
cat react_on_rails_pro/lib/react_on_rails_pro/version.rb | grep PROTOCOL_VERSION

echo
echo "=== Current HEAD: node-renderer protocolVersion ==="
cat packages/react-on-rails-pro-node-renderer/package.json | grep protocolVersion

echo
echo "=== Did targetBundles exist on origin/main in request.rb? ==="
git show origin/main:react_on_rails_pro/lib/react_on_rails_pro/request.rb 2>/dev/null | grep -c "targetBundles" || echo "0"

echo
echo "=== Current HEAD: does targetBundles still exist in request.rb? ==="
grep -c "targetBundles" react_on_rails_pro/lib/react_on_rails_pro/request.rb || echo "0 (outside upload_assets)"

Repository: shakacode/react_on_rails

Length of output: 512


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Did worker.ts change in this PR? Check for bundle_ parsing ==="
git show origin/main:packages/react-on-rails-pro-node-renderer/src/worker.ts 2>/dev/null | grep -n "bundle_" | head -10

echo
echo "=== Current HEAD worker.ts bundle_ parsing ==="
grep -n "bundle_" packages/react-on-rails-pro-node-renderer/src/worker.ts | head -10

echo
echo "=== Check if origin/main worker expected targetBundles ==="
git show origin/main:packages/react-on-rails-pro-node-renderer/src/worker.ts 2>/dev/null | grep -n "targetBundles" | head -5

echo
echo "=== Check if HEAD worker still references targetBundles ==="
grep -n "targetBundles" packages/react-on-rails-pro-node-renderer/src/worker.ts | head -5

Repository: shakacode/react_on_rails

Length of output: 1618


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== HEAD worker.ts: /upload-assets endpoint full context (lines 380-415) ==="
sed -n '380,415p' packages/react-on-rails-pro-node-renderer/src/worker.ts

echo
echo "=== HEAD worker.ts: how targetBundles is used in /upload-assets ==="
sed -n '380,430p' packages/react-on-rails-pro-node-renderer/src/worker.ts | grep -A 50 "targetBundles" | head -40

echo
echo "=== Check if /upload-assets actually uses targetBundles or ignores it ==="
sed -n '334,380p' packages/react-on-rails-pro-node-renderer/src/worker.ts

Repository: shakacode/react_on_rails

Length of output: 4991


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Origin/main /upload-assets endpoint (lines 317-360) ==="
git show origin/main:packages/react-on-rails-pro-node-renderer/src/worker.ts 2>/dev/null | sed -n '317,360p'

echo
echo "=== Verify: does new /upload-assets check for or use targetBundles? ==="
grep -A 30 "'/upload-assets'" packages/react-on-rails-pro-node-renderer/src/worker.ts | grep -i "targetBundles" || echo "Not found in /upload-assets"

Repository: shakacode/react_on_rails

Length of output: 2354


Verify and bump the protocol version for the /upload-assets wire-format change.

This PR changes the /upload-assets endpoint to derive bundle targets from bundle_<hash> form keys instead of the explicit targetBundles field. The old node renderer requires targetBundles and will reject requests lacking it—if a new gem is deployed with an older node renderer, /upload-assets will fail with "No targetBundles provided."

Both react_on_rails_pro/lib/react_on_rails_pro/version.rb and packages/react-on-rails-pro-node-renderer/package.json still declare protocolVersion = "2.0.0", but the protocols are now incompatible. Increment the protocol version in both files to signal to the worker that the request format has changed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@react_on_rails_pro/lib/react_on_rails_pro/request.rb` around lines 62 - 64,
The upload-assets wire format changed to derive bundle targets from
bundle_<hash> form keys (see perform_request("/upload-assets", form:
form_with_assets_and_bundle)), so bump the shared protocol version string used
by the gem and the node renderer (currently "2.0.0") to a new version (e.g.
"2.1.0") in both places where protocolVersion is declared so the worker can
detect the format change; update the Ruby protocol constant and the package.json
protocolVersion field to the new value and ensure any code that checks
protocolVersion is comparing against the new version.

Comment thread CHANGELOG.md

### [Unreleased]

#### Fixed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The PR link here says PR 2767 but this PR is #2768.

Suggested change
#### Fixed
- **[Pro] Fixed bundle duplication in remote node renderer asset uploads**: When RSC support is enabled, running `rake react_on_rails_pro:copy_assets_to_remote_vm_renderer` no longer duplicates bundle JS files across bundle directories. Previously, both the server bundle and RSC bundle were copied into every target directory; now each bundle is placed only in its own directory while shared assets (manifests, stats) are correctly distributed to all. [PR 2768](https://github.com/shakacode/react_on_rails/pull/2768) by [AbanoubGhadban](https://github.com/AbanoubGhadban). Fixes [Issue 2766](https://github.com/shakacode/react_on_rails/issues/2766).

Body: Record<string, Asset>;
}>('/upload-assets', async (req, res) => {
if (!(await requestPrechecks(req, res))) {
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a breaking protocol change — the endpoint no longer accepts targetBundles and now requires bundle_<hash> fields. The protocol version in package.json is still 2.0.0, so a checkProtocolVersion call won't catch a gem/renderer version mismatch. If a user updates only the node renderer (not the gem), they'll get a confusing 400 "No bundle_ fields provided" instead of a clear protocol-version error.

Consider bumping the minor protocol version (e.g. 2.1.0) so that the existing version-check middleware can surface a clear "gem/renderer version mismatch" message.

Comment on lines 54 to 60
if ReactOnRailsPro.configuration.enable_rsc_support
rsc_bundle_path = ReactOnRailsPro::Utils.rsc_bundle_js_file_path
unless File.exist?(rsc_bundle_path)
raise ReactOnRailsPro::Error, "RSC bundle not found at #{rsc_bundle_path}. " \
"Please build your bundles before uploading assets."
end
target_bundles << pool.rsc_bundle_hash
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This block is now redundant. form_with_assets_and_bundlepopulate_form_with_bundle_and_assets(form, check_bundle: true)add_bundle_to_form(..., check_bundle: true) already raises ReactOnRailsPro::Error, "Bundle not found #{rsc_bundle_path}" if the RSC bundle is missing.

The only visible difference is the error message (this block produces a more descriptive message). If the better message is important, consider moving the check to add_bundle_to_form instead of duplicating it here. Otherwise this block can be removed entirely.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 19, 2026

Review summary posted via inline comments. Top-level notes: (1) CHANGELOG references PR 2767 instead of 2768. (2) The /upload-assets protocol change (removing targetBundles, requiring bundle_hash fields) is breaking but the protocol version was not bumped from 2.0.0 — old gem + new renderer will get a confusing 400 rather than a version-mismatch error. Consider bumping to 2.1.0. (3) The RSC bundle existence check in upload_assets (request.rb lines 54-60) is now redundant since form_with_assets_and_bundle already checks it via check_bundle: true. (4) handleNewBundlesProvided runs handleNewBundleProvided concurrently for each bundle, all sharing the same assetsToCopy file paths. Safe today since copyUploadedAssets uses fs-extra copy (reads not moves), but worth a comment near Promise.allSettled. Overall the fix is correct: extractBundlesAndAssets cleanly extracts the bundle-vs-asset classification, moving copyUploadedAssets outside the EEXIST try/catch makes asset copying idempotent, and reusing handleNewBundlesProvided eliminates the duplicated lock logic in worker.ts.

providedNewBundles.push({ timestamp: primaryBundleTimestamp, bundle: value });
} else if (key.startsWith('bundle_')) {
assertAsset(value, key);
providedNewBundles.push({ timestamp: key.slice('bundle_'.length), bundle: value });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Edge case: empty timestamp from bundle_ with no suffix

If a form field is sent with the key "bundle_" (hash part omitted), key.slice('bundle_'.length) produces "". This empty string silently becomes the timestamp, which will likely create a bundle directory at an unexpected path or overwrite something unrelated.

Consider adding a guard:

Suggested change
providedNewBundles.push({ timestamp: key.slice('bundle_'.length), bundle: value });
} else if (key.startsWith('bundle_') && key.length > 'bundle_'.length) {

Or validate after the loop and return an error if any extracted timestamp is empty.

} else if (key.startsWith('bundle_')) {
assertAsset(value, key);
providedNewBundles.push({ timestamp: key.slice('bundle_'.length), bundle: value });
} else if (isAsset(value)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Naming collision risk with shared asset files

Any multipart field whose key starts with bundle_ is treated as a bundle, not a shared asset. If a user ever configures an assets_to_copy entry whose filename begins with bundle_ (e.g., a Webpack stats file named bundle_stats.json), it would be silently misclassified as a bundle — its content would be moved to a per-hash directory rather than copied to all bundle directories as an asset.

This is a non-obvious constraint that should be documented (e.g., in AGENTS.md or a JSDoc on extractBundlesAndAssets) so callers know asset filenames must not start with bundle_.

Comment on lines +131 to +136
// Always copy assets to the bundle directory — even if the bundle was
// already present (e.g., from a prior upload or another worker).
// copyUploadedAssets uses overwrite:true, so this is idempotent.
if (assetsToCopy) {
await copyUploadedAssets(assetsToCopy, bundleDirectory);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"Always copy" comment is slightly misleading for the lock-failure path

The comment says "Always copy assets … even if the bundle was already present", but assets are not copied when the lock cannot be acquired (line 102 returns an error before reaching this block). The "always" refers specifically to the case where the bundle file already exists on disk (file-exists catch branch at line 113), not to the lock-failure case.

Worth clarifying the comment so future readers don't mistakenly believe lock failure still copies assets:

Suggested change
// Always copy assets to the bundle directory — even if the bundle was
// already present (e.g., from a prior upload or another worker).
// copyUploadedAssets uses overwrite:true, so this is idempotent.
if (assetsToCopy) {
await copyUploadedAssets(assetsToCopy, bundleDirectory);
}
// Copy assets to the bundle directory — even if the bundle was already
// present on disk (e.g., from a prior upload or another worker).
// NOTE: If lock acquisition failed above, we return early and assets are
// not copied for that bundle.
// copyUploadedAssets uses overwrite:true, so this is idempotent.
if (assetsToCopy) {
await copyUploadedAssets(assetsToCopy, bundleDirectory);
}

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 19, 2026

Overall this is a solid, well-motivated refactor. The root bug (all uploaded files being blindly copied into every bundle directory) is real, the fix is correct, and extracting extractBundlesAndAssets is a clean DRY improvement. The protocol-version bump is appropriate for the breaking API change. A few things to address:

Substantive issues

1. Empty-timestamp edge case in extractBundlesAndAssets (see inline): A form field keyed "bundle_" (no hash) yields an empty-string timestamp. Nothing validates this, so it would create or clobber a directory at whatever path getRequestBundleFilePath('') resolves to. A one-line length guard would close this.

2. bundle_ prefix collision with shared asset filenames (see inline): Any asset whose filename starts with bundle_ would be silently misclassified as a bundle rather than copied to all directories. Low probability today, but it should be documented as a constraint (e.g., in a JSDoc on extractBundlesAndAssets) so future callers don't stumble on it.

3. "Always copy" comment is misleading for the lock-failure path (see inline): The new comment in handleRenderRequest.ts says assets are always copied, but they are skipped when lock acquisition fails (the function returns early at line 102). The behavior change is correct and intentional — this is just a clarity issue.

Minor observation

asset_exists_on_vm_renderer? in request.rb (around line 74–79) still builds a targetBundles array and sends it to /asset-exists using the protocol-2.0 style. That endpoint was not touched in this PR, which is fine if it deliberately stays on the old convention. Worth adding a short comment confirming this is intentional, or a TODO if it will be migrated later.

Positive highlights

Moving copyUploadedAssets outside the bundle-was-new branch (so assets are copied even when the bundle already existed on disk) is a meaningful correctness improvement. Reusing handleNewBundlesProvided in the /upload-assets handler instead of re-implementing per-bundle locking eliminates an entire class of drift-related bugs. Promise.allSettled is retained in both paths with a helpful explanatory comment.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

2 similar comments
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

AbanoubGhadban and others added 5 commits March 26, 2026 11:07
)

The /upload-assets endpoint treated all uploaded files as a flat list and
copied every file into every target bundle directory. When RSC support
sends both server and RSC bundles, each bundle JS was duplicated in the
other's directory.

- Extract shared `extractBundlesAndAssets` helper that parses form body
  into bundles (bundle_<hash> keys) and shared assets, used by both the
  render and /upload-assets endpoints
- Rewrite /upload-assets to reuse `handleNewBundlesProvided` from the
  render path, ensuring each bundle goes only to its own directory while
  shared assets are distributed to all directories
- Remove redundant `targetBundles` form field from /upload-assets
  (directories are now derived from bundle_<hash> keys)
- Update Ruby side to stop sending targetBundles for upload_assets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When handleNewBundleProvided encounters an existing bundle (EEXIST),
it previously skipped copying assets. This caused the upload_asset_spec
integration test to fail because a pre-existing bundle meant assets
were never written. Since copyUploadedAssets uses overwrite:true,
always copying is idempotent and safe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix CHANGELOG PR link from 2767 to 2768
- Bump protocol version to 2.1.0 in both gem and node-renderer
  (wire-format change: /upload-assets no longer uses targetBundles)
- Fix Body type to Record<string, unknown> (matches actual form contents)
- Use slice() instead of replace() for bundle_ prefix stripping
- Add comment explaining intentionally duplicated bundle existence checks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A form field keyed exactly "bundle_" (no hash) could indicate a bug
in the Ruby client returning an empty bundle hash. Log a warning and
drop the field rather than creating a directory with an empty name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The parameter is used for error message context, not as actual JS code.
Renaming makes it accurate for both render (JS code) and upload-assets
(task description) callers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
import packageJson from '../package.json';
import worker, { disableHttp2 } from '../src/worker';
import { resetForTest, serverBundleCachePath, getFixtureBundle } from './helper';
import { resetForTest, serverBundleCachePath, getFixtureBundle, getFixtureSecondaryBundle } from './helper';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unused import

getFixtureSecondaryBundle is imported here but never referenced anywhere in this file (it's only used in worker.test.ts). This will produce a lint warning (no-unused-vars) and should be removed.

Suggested change
import { resetForTest, serverBundleCachePath, getFixtureBundle, getFixtureSecondaryBundle } from './helper';
import { resetForTest, serverBundleCachePath, getFixtureBundle } from './helper';

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 26, 2026

Review

The core fix is correct and well-motivated: the old /upload-assets handler blindly copied every uploaded file into every target bundle directory, so enabling RSC (two bundles) would duplicate both JS files into both directories. The new bundle_<hash> form-key convention paired with reuse of handleNewBundlesProvided cleanly solves this. The extractBundlesAndAssets extraction also neatly DRYs up the render and upload paths.

A few issues worth addressing before merge:

Security (medium)

Path traversal via bundle_<hash> key - the hash suffix is used verbatim as a path component in getRequestBundleFilePath -> path.join(serverBundleCachePath, timestamp, ...). An authenticated caller sending bundle_../../evil can escape the cache directory. See inline comment at worker.ts:118.

Code quality (medium)

assertAsset can throw outside the try/catch - extractBundlesAndAssets is called before the try block in both the render handler (line 309) and the /upload-assets handler (line 350). A malformed bundle_<hash> field (non-file value) would produce a generic Fastify 500 instead of the custom errorResponseResult format. See inline comment at worker.ts:350.

Test / minor

  • Unused import in uploadRaceCondition.test.ts - getFixtureSecondaryBundle is imported but never used in this file (only in worker.test.ts). This will fail the no-unused-vars lint rule. See inline comment at uploadRaceCondition.test.ts:26.
  • No test for targetBundles-only requests to the new node renderer - the backward-compat test covers the case where both bundle_<hash> and targetBundles are present. It would be worth adding a test (or at least a note in the TODO comment) documenting what happens when an old Ruby client sends only targetBundles to the new node renderer - based on the code it returns a 400-style error, which could silently break deployments where the gem is upgraded without the node renderer.

Minor observations

  • Moving copyUploadedAssets to after the inner try/catch (but still inside the outer lock try/finally) is the right call - assets are now copied even in the "another worker beat us to the bundle" race case. The PR description says "after the lock's finally block" but the code correctly keeps it inside the outer try, so the lock is still held during the copy.
  • Protocol version bump to 2.1.0 is mentioned in the CodeRabbit summary but not visible in the diff - worth confirming package.json is updated.

@AbanoubGhadban
Copy link
Copy Markdown
Collaborator Author

/run-skipped-ci

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Full CI Mode Enabled

Triggered all workflows for full CI coverage

Note: Added the full-ci label to this PR. All future commits will run the full CI suite (including minimum dependency tests) until the label is removed.

To disable full CI mode, use the /stop-run-skipped-ci command.

View progress in the Actions tab.

@AbanoubGhadban AbanoubGhadban merged commit 9b30cc1 into main Mar 26, 2026
63 checks passed
@AbanoubGhadban AbanoubGhadban deleted the fix/2766-upload-assets-bundle-filtering branch March 26, 2026 09:47
justin808 added a commit that referenced this pull request Mar 27, 2026
…olve-2833

* origin/main:
  docs: fix profiling node renderer command (#2863)
  generators: point Pro install fallback to upgrade guide (#2868)
  Add RSC Flight payload optimization guide (Article 7) (#2827)
  Migrate from deprecated Async::Variable to Async::Promise (#2832)
  docs: turn pro quick start into a gateway (#2862)
  Fix upload-assets endpoint duplicating bundles across directories (#2768)
  docs: fix stale docs links and help URLs (#2850)
  docs: replace dead pro.reactonrails.com links (#2851)
  docs: refresh generator and helper URLs (#2852)
  Add standalone RSC upgrade guide for existing Pro apps (#2831)
  Raise docs version floor to 16.4.0 in install/demo guidance (#2610)

# Conflicts:
#	CHANGELOG.md
justin808 added a commit that referenced this pull request Mar 27, 2026
…olve-2834

* origin/main:
  docs: fix profiling node renderer command (#2863)
  generators: point Pro install fallback to upgrade guide (#2868)
  Add RSC Flight payload optimization guide (Article 7) (#2827)
  Migrate from deprecated Async::Variable to Async::Promise (#2832)
  docs: turn pro quick start into a gateway (#2862)
  Fix upload-assets endpoint duplicating bundles across directories (#2768)
  docs: fix stale docs links and help URLs (#2850)
  docs: replace dead pro.reactonrails.com links (#2851)
  docs: refresh generator and helper URLs (#2852)

# Conflicts:
#	CHANGELOG.md
#	react_on_rails/lib/react_on_rails/utils.rb
justin808 added a commit that referenced this pull request Mar 27, 2026
…olve-2849

* origin/main:
  docs: fix profiling node renderer command (#2863)
  generators: point Pro install fallback to upgrade guide (#2868)
  Add RSC Flight payload optimization guide (Article 7) (#2827)
  Migrate from deprecated Async::Variable to Async::Promise (#2832)
  docs: turn pro quick start into a gateway (#2862)
  Fix upload-assets endpoint duplicating bundles across directories (#2768)
  docs: fix stale docs links and help URLs (#2850)
  docs: replace dead pro.reactonrails.com links (#2851)
  docs: refresh generator and helper URLs (#2852)
  Add standalone RSC upgrade guide for existing Pro apps (#2831)
  Raise docs version floor to 16.4.0 in install/demo guidance (#2610)

# Conflicts:
#	CHANGELOG.md
justin808 added a commit that referenced this pull request Mar 27, 2026
…olve-2835

* origin/main: (21 commits)
  docs: fix profiling node renderer command (#2863)
  generators: point Pro install fallback to upgrade guide (#2868)
  Add RSC Flight payload optimization guide (Article 7) (#2827)
  Migrate from deprecated Async::Variable to Async::Promise (#2832)
  docs: turn pro quick start into a gateway (#2862)
  Fix upload-assets endpoint duplicating bundles across directories (#2768)
  docs: fix stale docs links and help URLs (#2850)
  docs: replace dead pro.reactonrails.com links (#2851)
  docs: refresh generator and helper URLs (#2852)
  Add standalone RSC upgrade guide for existing Pro apps (#2831)
  Raise docs version floor to 16.4.0 in install/demo guidance (#2610)
  Fix release script: require changelog, fix RC version computation (#2848)
  Bump version to 16.5.0
  Bump version to 16.5.0.rc.0
  Update CHANGELOG.md for 16.5.0.rc.0 (#2847)
  Docs: add memory leak prevention guide for Node Renderer SSR (#2845)
  Docs: fix RSC migration gaps found during real-world migration (#2842)
  Add common mistakes sections to RSC migration guides (#2826)
  fix: preserve runtime env vars across Bundler.with_unbundled_env (#2836)
  Skip Pro CI workflows for Dependabot PRs (#2825)
  ...

# Conflicts:
#	CHANGELOG.md
@justin808 justin808 mentioned this pull request Mar 28, 2026
2 tasks
justin808 added a commit that referenced this pull request Mar 28, 2026
## Summary

- Stamped `### [16.5.1] - 2026-03-27` with two Pro fixes (PRs #2872,
#2768)
- Removed the `### [16.5.0.rc.0]` section (was already released as
16.5.0 stable)
- Consolidated the 16.5.0 section with full entries (previously just
said "no changes from rc.0")
- Updated diff links at bottom of file

## Skipped PRs (docs/internal only)
#2856, #2860, #2857, #2859, #2864, #2870, #2863, #2868, #2827, #2862,
#2850, #2851, #2852, #2831, #2610, #2848

## Test plan
- [ ] Verify CHANGELOG.md formatting and diff links are correct
- [ ] Run `rake release` (no args) after merge to publish 16.5.1

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: changes are limited to `CHANGELOG.md` release notes and
version/compare links with no runtime code modifications.
> 
> **Overview**
> Documents the `16.5.1` release by adding a new section under
*Unreleased* with two **[Pro]** fixes (missing packaged rake tasks and
avoiding duplicated bundles during remote renderer asset uploads).
> 
> Cleans up the `16.5.0` entry by removing the `16.5.0.rc.0` section and
updating the compare-link footer so `unreleased` now starts from
`v16.5.1` and `16.5.0` compares from `v16.4.0`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9a6b224. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

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

## Summary by CodeRabbit

* **Documentation**
* Updated changelog to document version 16.5.1 release with consolidated
version history and updated comparison links.

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

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
justin808 added a commit that referenced this pull request Mar 28, 2026
## Summary

- Stamped `### [16.5.1] - 2026-03-27` with two Pro fixes (PRs #2872,
#2768)
- Removed the `### [16.5.0.rc.0]` section (was already released as
16.5.0 stable)
- Consolidated the 16.5.0 section with full entries (previously just
said "no changes from rc.0")
- Updated diff links at bottom of file

## Skipped PRs (docs/internal only)
#2856, #2860, #2857, #2859, #2864, #2870, #2863, #2868, #2827, #2862,
#2850, #2851, #2852, #2831, #2610, #2848

## Test plan
- [ ] Verify CHANGELOG.md formatting and diff links are correct
- [ ] Run `rake release` (no args) after merge to publish 16.5.1

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: changes are limited to `CHANGELOG.md` release notes and
version/compare links with no runtime code modifications.
> 
> **Overview**
> Documents the `16.5.1` release by adding a new section under
*Unreleased* with two **[Pro]** fixes (missing packaged rake tasks and
avoiding duplicated bundles during remote renderer asset uploads).
> 
> Cleans up the `16.5.0` entry by removing the `16.5.0.rc.0` section and
updating the compare-link footer so `unreleased` now starts from
`v16.5.1` and `16.5.0` compares from `v16.4.0`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9a6b224. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

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

## Summary by CodeRabbit

* **Documentation**
* Updated changelog to document version 16.5.1 release with consolidated
version history and updated comparison links.

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

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AbanoubGhadban added a commit that referenced this pull request Apr 1, 2026
…ndpoint

Three test fixes for API changes from main (PR #2768):

1. vm.test.ts: Replace direct `buildVM()` calls with `buildExecutionContext()`
   since `buildVM` is no longer exported (now a private function).

2. worker.test.ts: Update empty upload-assets test to expect 400 status
   since the endpoint now requires at least one `bundle_<hash>` field.

3. worker.test.ts: Update targetBundles directory test since the endpoint
   no longer creates directories for targetBundles (only bundle_<hash>
   fields determine directory placement). Also update buildVM spy to use
   buildExecutionContext which is the exported function.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
justin808 pushed a commit that referenced this pull request Apr 12, 2026
…ndpoint

Three test fixes for API changes from main (PR #2768):

1. vm.test.ts: Replace direct `buildVM()` calls with `buildExecutionContext()`
   since `buildVM` is no longer exported (now a private function).

2. worker.test.ts: Update empty upload-assets test to expect 400 status
   since the endpoint now requires at least one `bundle_<hash>` field.

3. worker.test.ts: Update targetBundles directory test since the endpoint
   no longer creates directories for targetBundles (only bundle_<hash>
   fields determine directory placement). Also update buildVM spy to use
   buildExecutionContext which is the exported function.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
justin808 pushed a commit that referenced this pull request Apr 12, 2026
…ndpoint

Three test fixes for API changes from main (PR #2768):

1. vm.test.ts: Replace direct `buildVM()` calls with `buildExecutionContext()`
   since `buildVM` is no longer exported (now a private function).

2. worker.test.ts: Update empty upload-assets test to expect 400 status
   since the endpoint now requires at least one `bundle_<hash>` field.

3. worker.test.ts: Update targetBundles directory test since the endpoint
   no longer creates directories for targetBundles (only bundle_<hash>
   fields determine directory placement). Also update buildVM spy to use
   buildExecutionContext which is the exported function.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AbanoubGhadban added a commit that referenced this pull request May 3, 2026
…ndpoint

Three test fixes for API changes from main (PR #2768):

1. vm.test.ts: Replace direct `buildVM()` calls with `buildExecutionContext()`
   since `buildVM` is no longer exported (now a private function).

2. worker.test.ts: Update empty upload-assets test to expect 400 status
   since the endpoint now requires at least one `bundle_<hash>` field.

3. worker.test.ts: Update targetBundles directory test since the endpoint
   no longer creates directories for targetBundles (only bundle_<hash>
   fields determine directory placement). Also update buildVM spy to use
   buildExecutionContext which is the exported function.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

upload-assets endpoint copies all files into all target bundle directories, duplicating bundles

1 participant