Skip to content

Flush RSC payloads before incomplete HTML tails - #4379

Merged
justin808 merged 2 commits into
mainfrom
codex/batch-e-pro-rsc-stream
Jul 3, 2026
Merged

Flush RSC payloads before incomplete HTML tails#4379
justin808 merged 2 commits into
mainfrom
codex/batch-e-pro-rsc-stream

Conversation

@justin808

@justin808 justin808 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #4327.

injectRSCPayload used to skip an entire flush whenever streamed HTML ended with an incomplete HTML or UTF-8 tail. That let complete HTML prefixes, RSC initialization, payload scripts, stylesheet buffers, and observability marks accumulate behind a small incomplete tail. This keeps only the incomplete tail buffered while flushing the complete prefix and ready RSC data.

Changes

  • Track the exact incomplete UTF-8 or HTML tag tail as a retained buffer.
  • Flush complete HTML prefixes, RSC initialization, stylesheet buffers, payload scripts, and observability marks while preserving the tail for the next chunk.
  • Preserve deferred Suspense reveal HTML ordering and append retained tails after each successful flush.
  • Add regression coverage for an observability split-tag tail where the first output chunk now flushes safe HTML and RSC scripts.

Validation

  • pnpm --dir packages/react-on-rails-pro exec jest tests/injectRSCPayload.test.ts --runInBand
  • pnpm --dir packages/react-on-rails-pro run test:streaming
  • pnpm --dir packages/react-on-rails-pro run type-check
  • pnpm exec prettier --check packages/react-on-rails-pro/src/injectRSCPayload.ts packages/react-on-rails-pro/tests/injectRSCPayload.test.ts
  • pnpm exec eslint packages/react-on-rails-pro/src/injectRSCPayload.ts packages/react-on-rails-pro/tests/injectRSCPayload.test.ts --no-warn-ignored
  • script/check-pro-license-headers
  • git diff --check origin/main...HEAD
  • Read-only second review subagent: no actionable issues; reran focused Jest and diff-check.

No changelog entry: internal Pro streaming robustness fix with no public API change.

Summary by CodeRabbit

  • Bug Fixes
    • Hardened streamed rendering so incomplete HTML and UTF-8 byte fragments are preserved between flush cycles, avoiding dropped or corrupted markup.
    • Improved stylesheet preload handling during streaming, including safer promotion when preload/link markup is split across chunks.
    • Corrected boundary handling for raw-text elements (e.g., script/style/textarea/title) and refined opt-in observability injection to prevent malformed output at split points.
  • Tests
    • Expanded and tightened streamed boundary, stylesheet gating, and UTF-8 safety coverage, including edge cases around chunked tag/byte splits and flush ordering.

Streaming Behavior Note

This fix deliberately buffers open raw-text, template, and foreign-content containers until their browser-visible closing boundary is known, so RSC scripts are not emitted inside those containers. Plain text split on incomplete UTF-8 bytes keeps only the incomplete byte tail buffered while the UTF-8-safe prefix can flush.

Merge Readiness Note

  • Release mode: development from release tracker Release gate: react_on_rails 17.0.0 #3823; target branch main is beta phase. Accelerated-RC Agent Merge Confidence block is not required.
  • Review coverage for current head 3ad31b1695924de6836ac72a1e9c993464fc96cd: Claude review check passed and CodeRabbit check passed. Codex review hit usage limits after stale review artifacts; Greptile has only stale review/comment artifacts for earlier heads. Coverage floor still has two working systems; degraded systems are recorded here per AGENTS.md.
  • Current-head gates before merge: pr-ci-readiness 4379 reported READY, GraphQL unresolved review threads count was 0, and strict script/pr-merge-ledger 4379 --strict --changelog-classification not_user_visible --finding-dispositions /tmp/ror-batch-e-dispositions-4379.json is the final ledger gate.

@justin808

Copy link
Copy Markdown
Member Author

+ci-status

@justin808

Copy link
Copy Markdown
Member Author

+ci-run-hosted

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: flushing safe RSC payloads ahead of incomplete HTML tails.
✨ 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 codex/batch-e-pro-rsc-stream

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.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

CI Status

Head SHA: db83c3e60cdd
Changed files: 2
Docs-only heuristic (matches ci-changes-detector metadata paths): no
ready-for-hosted-ci label: absent
force-full-hosted-ci label: absent
Current hosted-CI waiver: not present for this SHA

Only the required gate is active unless hosted CI is requested.

@justin808
justin808 force-pushed the codex/batch-e-pro-rsc-stream branch from db83c3e to 8276ba4 Compare July 2, 2026 07:33
@justin808

Copy link
Copy Markdown
Member Author

Rebased onto origin/main b194bb67f after main advanced.

Post-rebase validation:

  • pnpm --dir packages/react-on-rails-pro exec jest tests/injectRSCPayload.test.ts --runInBand
  • pnpm --dir packages/react-on-rails-pro run test:streaming
  • pnpm --dir packages/react-on-rails-pro run type-check
  • git diff --check origin/main...HEAD

@justin808 justin808 added the ready-for-hosted-ci Run optimized hosted GitHub CI for this PR label Jul 2, 2026
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR changes RSC payload flushing so safe output can move past incomplete tails. The main changes are:

  • Incomplete UTF-8 and HTML tag tails are retained separately.
  • Complete HTML prefixes can flush with RSC initialization, payload scripts, stylesheets, and marks.
  • Deferred Suspense reveal HTML and retained tails are re-added after each flush.
  • Tests cover split tag, observability, and split UTF-8 streaming cases.

Confidence Score: 5/5

This looks safe to merge after considering the small malformed-UTF8 hardening note.

  • No blocking issues found in the changed code.
  • The main split-tag and split-UTF8 paths are covered by tests.
  • One edge case can emit a replacement character for malformed repeated UTF-8 lead bytes.

Important Files Changed

Filename Overview
packages/react-on-rails-pro/src/injectRSCPayload.ts Reworks incomplete-tail handling so complete prefixes flush while unsafe tail bytes remain buffered.
packages/react-on-rails-pro/tests/injectRSCPayload.test.ts Updates streaming assertions and adds coverage for flushing RSC scripts while a split observability tag tail is retained.

Reviews (1): Last reviewed commit: "Flush RSC payloads before incomplete HTM..." | Re-trigger Greptile

Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db83c3e60c

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts
@justin808
justin808 force-pushed the codex/batch-e-pro-rsc-stream branch from 8276ba4 to 7610093 Compare July 2, 2026 07:40
@justin808

Copy link
Copy Markdown
Member Author

Addressed current-head review feedback for split raw-text closing tags. injectRSCPayload now retains the whole open raw-text element when observability sees a split closing tag such as </scr, so generated RSC scripts do not flush while the browser is still inside app script/style text.

Validation after the fix:

  • pnpm --dir packages/react-on-rails-pro exec jest tests/injectRSCPayload.test.ts --runInBand
  • pnpm --dir packages/react-on-rails-pro run test:streaming
  • pnpm --dir packages/react-on-rails-pro run type-check
  • pnpm exec prettier --check packages/react-on-rails-pro/src/injectRSCPayload.ts packages/react-on-rails-pro/tests/injectRSCPayload.test.ts
  • pnpm exec eslint packages/react-on-rails-pro/src/injectRSCPayload.ts packages/react-on-rails-pro/tests/injectRSCPayload.test.ts --no-warn-ignored
  • script/check-pro-license-headers
  • git diff --check origin/main...HEAD

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7610093f2c

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts
Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts Outdated
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 63.52 KB (0%)
react-on-rails/client bundled (gzip) (time) 63.52 KB (0%)
react-on-rails/client bundled (brotli) 54.52 KB (+0.06% 🔺)
react-on-rails/client bundled (brotli) (time) 54.49 KB (0%)
react-on-rails-pro/client bundled (gzip) 64.54 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 64.54 KB (0%)
react-on-rails-pro/client bundled (brotli) 55.53 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 55.53 KB (0%)
registerServerComponent/client bundled (gzip) 75.91 KB (0%)
registerServerComponent/client bundled (gzip) (time) 75.91 KB (0%)
registerServerComponent/client bundled (brotli) 65.4 KB (0%)
registerServerComponent/client bundled (brotli) (time) 65.4 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 68.37 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 68.37 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 58.76 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 58.76 KB (0%)

@justin808
justin808 force-pushed the codex/batch-e-pro-rsc-stream branch from 7610093 to 0d374d1 Compare July 2, 2026 07:46
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@justin808

Copy link
Copy Markdown
Member Author

+ci-status

@justin808

Copy link
Copy Markdown
Member Author

+ci-run-hosted

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/react-on-rails-pro/src/injectRSCPayload.ts`:
- Around line 270-296: The raw-text tail detection in
findRawTextElementStartForSplitClosingTag is too permissive and can pick the
wrong tag when the closing prefix matches multiple raw-text tags, causing
premature flushes. Update the prefix matching so it keeps the correct raw-text
tag for the current incomplete closing tag, and change the opener/closer scan to
return the first unmatched opening tag after the last completed closing tag
instead of the last opening match.
🪄 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: be78e7fb-df75-4161-bd98-904e581ba9d6

📥 Commits

Reviewing files that changed from the base of the PR and between e03a4ad and 0d374d1.

📒 Files selected for processing (2)
  • packages/react-on-rails-pro/src/injectRSCPayload.ts
  • packages/react-on-rails-pro/tests/injectRSCPayload.test.ts

Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts Outdated
Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts Outdated
@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review

Focused on packages/react-on-rails-pro/src/injectRSCPayload.ts / tests/injectRSCPayload.test.ts (the only changed files).

What's solid

  • The byte-precise UTF-8 tail detection (findIncompleteUTF8TailStartIndex / findPreviousIncompleteUTF8TailStartIndex) correctly walks back across multiple consecutive incomplete-looking lead bytes (not just the last one), which fixes a real gap in the previous single-pass boolean check — verified by tracing the new "adjacent incomplete UTF-8 lead bytes" test by hand.
  • applyStreamedStylesheetPreloadGating's buffer math reuses subarrays of the original input buffer wherever the stylesheet-promotion regex made no change, so a benign side effect of the internal .toString() conversion (e.g. U+FFFD substitution for genuinely malformed bytes that end up fully inside the "complete" region) never leaks into the actual output bytes.
  • flush()'s buffer-retention ordering (deferredRevealHtmlBuffer before incompleteHtmlTailBuffer) matches the true byte order of the original stream, and the new retainDeferredHtmlAndIncompleteTail() early-return paths correctly avoid losing buffered RSC init/payload data when nothing is flushable yet — this is the actual bug ([Pro npm] injectRSCPayload flush() skips flushing everything (init scripts, complete HTML prefix, RSC payload scripts) when the HTML tail is incomplete, redoing O(buffer) work per retry #4327) being fixed, and the fix looks structurally correct for the cases the new tests cover.

Bug found

Left an inline comment on findRawTextElementStartForSplitClosingTag (~line 271-297): resolving the raw-text tag name from a short/ambiguous prefix (RAW_TEXT_TAG_NAMES.find(t => t.startsWith(closingName))) silently fails for <style>/<title> when the chunk boundary splits their closing tag right after </s or </t (ambiguous with script/textarea, which sort first), and fails for any raw-text tag when the boundary lands right after </ (empty prefix, short-circuited by if (!closingName) return undefined;). In both cases the code falls back to the pre-fix behavior this PR is meant to eliminate — flushing a still-open raw-text element as "complete" — which can let an RSC payload/init or observability mark script land inside it as inert text instead of executing. Since React's internal ~4KB write boundary can land at any byte offset, this is a real (if narrow) gap, not just theoretical, and isn't covered by the PR's new tests (which only exercise unambiguous 3+ character splits like </scr).

No other correctness issues found in the diff; nothing security-sensitive changed (no new user-input handling), and no obvious performance regressions — the added backward-scan loops are bounded by the (small, ≤ a few bytes) incomplete-tail length, not the full buffer.

Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts Outdated
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

CI Status

Head SHA: 0d374d18a287
Changed files: 2
Docs-only heuristic (matches ci-changes-detector metadata paths): no
ready-for-hosted-ci label: present
force-full-hosted-ci label: absent
Current hosted-CI waiver: not present for this SHA

Optimized hosted CI is enabled for this PR.

@justin808
justin808 force-pushed the codex/batch-e-pro-rsc-stream branch from 0d374d1 to 9928218 Compare July 2, 2026 09:28
@justin808

Copy link
Copy Markdown
Member Author

+ci-status

@justin808

Copy link
Copy Markdown
Member Author

+ci-run-hosted

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99282180e3

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts Outdated
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

CI Status

Head SHA: 99282180e3c4
Changed files: 2
Docs-only heuristic (matches ci-changes-detector metadata paths): no
ready-for-hosted-ci label: present
force-full-hosted-ci label: absent
Current hosted-CI waiver: not present for this SHA

Optimized hosted CI is enabled for this PR.

@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Summary

Reviewed the streaming-tail retention logic in injectRSCPayload.ts (the only substantive change file). The core fix — splitting off only the true incomplete UTF-8/tag tail instead of gating the whole flush — is structurally sound, and the byte-vs-string offset math in applyStreamedStylesheetPreloadGating checks out (traced by hand for several UTF-8 boundary cases).

One correctness bug found (left as an inline comment on findUnclosedRawTextElementStart, lines 268-289): the raw-text open/closing-tag scan has no notion of "currently inside a raw-text element's content," so a <script>/<style> element whose own text content contains an unbalanced tag-like substring (e.g. <script>const msg = "Use <style> tags for CSS.";</script>) gets misidentified as having an unclosed element. This causes the tail-retention logic to buffer all subsequent HTML/RSC data for the rest of the response instead of just the true incomplete tail — effectively reintroducing the over-buffering bug this PR fixes, but via a different trigger. It's gated behind rscStreamObservability: true (the same mode this PR's new tests target), and isn't covered by the added regression tests, which only exercise tags split across chunk boundaries, not tag-like text embedded within a raw-text element's own content.

No security concerns (no new user-input handling), and no other performance regressions — the backward-scan loops are bounded by the small retained-tail size, not the full buffer.

Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts Outdated
Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts Outdated
Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts Outdated
Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts Outdated
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review summary

I focused on packages/react-on-rails-pro/src/injectRSCPayload.ts, the hand-rolled streaming HTML tokenizer this PR adds (raw-text/foreign-content/template/comment/CDATA/script-double-escape tracking) plus the buffer-flush orchestration in flush() / applyStreamedStylesheetPreloadGating(). The tokenizer state machine itself is well-tested — the test file has dedicated coverage for nearly every tricky HTML edge case (nested templates, foreign content, script double-escaping, CDATA, quoted attributes, etc.), and I didn't find defects there.

One real correctness bug, left as inline comments on the relevant lines:

  • In applyStreamedStylesheetPreloadGating, both the 'tag' (observability) and 'link' (default) branches contain an early return { gatedHtmlBuffer: Buffer.alloc(0), incompleteHtmlTailBuffer: html, ... } that fires whenever a chunk boundary splits a UTF-8 multi-byte character in plain text (no incomplete tag involved). This discards the entire accumulated buffer instead of flushing the already-valid, tag-complete prefix and retaining only the few UTF-8 continuation bytes — which is exactly what the general code path a few lines below already does correctly. This reproduces the core symptom of [Pro npm] injectRSCPayload flush() skips flushing everything (init scripts, complete HTML prefix, RSC payload scripts) when the HTML tail is incomplete, redoing O(buffer) work per retry #4327 (this PR's stated target) for what's likely the most common trigger of a mid-character split: ordinary internationalized text (accents, CJK, emoji) landing on one of React's arbitrary 4KB write boundaries. It affects both modes, so it hits every app by default (not just rscStreamObservability: true).
  • The existing UTF-8-tail tests ('keeps default RSC scripts out of split UTF-8 text tails', 'keeps observability scripts out of split UTF-8 text tails', 'preserves split UTF-8 bytes when promoting streamed RSC stylesheet preloads') only assert on the final joined output (collectStreamData/collectStreamBuffer), so they pass regardless of whether the flush was eager or fully delayed. Only the tag-split regression test ('flushes complete HTML and RSC scripts while holding an observability split tag tail') checks firstChunk timing — an equivalent per-chunk assertion for the plain-UTF-8-split case would have caught this.

One minor code-quality nit: in scanForeignContentTail (and mirrored in scanTemplateTail's foreign-content handling), the parsedTag.tagName === tagName && parsedTag.isSelfClosingStartTag branch is unreachable dead code, since isSelfClosingStartTag is only ever true when !isClosingTag, which the preceding branch already fully covers.

Everything else (buffer ordering in flush(), the fallback-timer cancellation logic, the Suspense-reveal deferral/retained-scan-state interaction, RSC promise settlement/error handling) checked out as correct on a careful read.

@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review: Flush RSC payloads before incomplete HTML tails

This PR replaces the old all-or-nothing splitIncompleteHtmlTagTail (a naive lastIndexOf('<') vs lastIndexOf('>') check) with a ~900-line hand-rolled HTML tail-scanning state machine that tracks raw-text elements, comments, CDATA, <template>, and foreign content (svg/math) so complete HTML/RSC/stylesheet/observability content can flush even when the chunk boundary splits a tag or a UTF-8 byte sequence. Reviewed via 8 parallel finder passes + direct verification against the actual PR diff (note: the local checkout in this environment was stale relative to the PR's real HEAD 9049f4f, so every finding below was re-verified directly against gh pr diff 4379 output, not the checked-out files — two initially-flagged issues turned out to be stale-checkout artifacts and were discarded).

Findings (most severe first)

1. injectRSCPayload.ts (applyStreamedStylesheetPreloadGating, ~line 1418-1424 and ~1438-1444) — Correctness: silently reverts to full-buffer withholding for the exact case this PR is meant to fix

When a chunk's tag/raw-text scan resolves cleanly (nextIncompleteHtmlTagTail is empty — no open tag) but a trailing UTF-8 byte sequence is still incomplete, both the 'tag' and 'link' mode branches throw away the already-computed completeHtml/gatedHtml result and return:

return {
  gatedHtmlBuffer: Buffer.alloc(0),
  incompleteHtmlTailBuffer: html,              // the ENTIRE original buffer, not just the tail
  incompleteHtmlTailScanState: retainedTailScanState, // the stale input, not nextIncompleteHtmlTailScanState
};

This reproduces the old "skip the entire flush" bug for plain text containing a split multi-byte character with no open tag nearby (e.g. an accented character or emoji landing on a chunk boundary) — exactly the scenario the PR's own new tests 'keeps default RSC scripts out of split UTF-8 text tails' and 'keeps observability scripts out of split UTF-8 text tails' construct, but those tests only assert on the final concatenated stream (toContain), not on per-chunk flush timing, so they don't catch that the "before...café" prefix — and any RSC payload/stylesheet/observability content on that chunk — is needlessly withheld until the next chunk arrives. The already-correct completeHtmlBuffer/incompleteHtmlTailBuffer split computed a few lines later in the same function (lines ~1205-1211) should be used here instead of discarding the work and falling back to the pre-PR behavior.

2. injectRSCPayload.test.ts (~line 714) — Test coverage: ordering assertion weakened at the riskiest point in the diff

-    expect(resultStr).toContain('before<script>after</script>');
+    expect(resultStr).toContain('before');
+    expect(resultStr).toContain('<script>after</script>');

The original single-string assertion enforced that 'before' and the completed <script> tag appear contiguously/in order. Splitting it into two independent toContain calls drops that ordering guarantee. Every other new UTF-8/raw-text test this PR adds carefully asserts relative ordering via indexOf/toBeLessThan, but this pre-existing test was weakened rather than upgraded to match — meaning an out-of-order-flush regression in the new retained-tail bookkeeping (the class of bug this rewrite is most likely to introduce) would not be caught here.

3. injectRSCPayload.ts (scanForeignContentTail ~line 590, scanTemplateTail ~line 767) — Reuse: ~150-200 lines of near-duplicated state machine

The two functions are structurally identical for the comment/cdata/rawText/template resume branches (copy-pasted verbatim), differing only in the tag-match condition and the self-closing-tag handling foreign content needs. They already show drift: scanForeignContentTail's recursive-template branch carries explanatory eslint-disable-next-line no-use-before-define comments that the equivalent block in scanTemplateTail lacks. Worth factoring into one depth/tagName-parameterized scanner.

4. injectRSCPayload.ts (splitIncompleteHtmlTagTail, splitIncompleteLinkOrRawTextTail) — Efficiency: redundant double full-buffer scan on every flush

Both functions unconditionally run splitUnclosedRawTextElementTail (a full tag-walking pass) and then, whenever that returns "nothing incomplete," unconditionally run a second, independent full tag-walking pass (splitTrailingIncompleteHtmlTagTail / splitIncompleteLinkTagTail) over the same string. Since retainedTailScanState is typically undefined (most flushes land on complete markup), this doubled scan runs on effectively every streamed chunk of every SSR request, not just pathological ones.

5. Missing CHANGELOG.md entry

AGENTS.md: "Update /CHANGELOG.md for user-visible changes only (features, bug fixes, breaking changes, deprecations, performance improvements). Do not add entries for linting, formatting, refactoring, tests, or doc fixes." This PR fixes #4327, a bug where streamed markup was dropped/corrupted — a user-visible bug fix, not one of the listed exemptions. The PR body's justification ("internal Pro streaming robustness fix with no public API change") isn't the actual exemption criterion in AGENTS.md.

6. injectRSCPayload.ts (findRawTextClosingTagSearchStart, ~line 539) — Efficiency: recompiles a RegExp per call

const closingTagPrefixPattern = new RegExp(`</${escapeRegExpLiteral(tagName)}(?=[\\s>])`, 'gi');

This builds and compiles a new RegExp on every invocation instead of reusing RAW_TEXT_CLOSING_TAG_PREFIX_PATTERNS.get(tagName), which already holds the equivalent precompiled pattern (missing only the / in the lookahead, since this is the "search backward from the end" fallback). For a raw-text element (large <style>/<textarea>) streamed across many small chunks, this recompiles on every chunk while the element stays open.


Overall the core approach (retain only the true incomplete tail instead of the whole buffer) is sound, and the state-machine handles a wide variety of split-boundary cases the old code didn't. Finding 1 is the one worth blocking on — it's a real regression relative to the PR's own stated goal, in the most common real-world trigger (multi-byte characters in plain text near a flush boundary).

Comment thread packages/react-on-rails-pro/src/injectRSCPayload.ts Outdated
Comment thread packages/react-on-rails-pro/tests/injectRSCPayload.test.ts Outdated
@justin808
justin808 force-pushed the codex/batch-e-pro-rsc-stream branch from 9049f4f to 3ad31b1 Compare July 3, 2026 01:40
@justin808

Copy link
Copy Markdown
Member Author

+ci-status

@justin808

Copy link
Copy Markdown
Member Author

+ci-run-hosted

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

CI Status

Head SHA: 3ad31b169592
Changed files: 2
Docs-only heuristic (matches ci-changes-detector metadata paths): no
ready-for-hosted-ci label: present
force-full-hosted-ci label: absent
Current hosted-CI waiver: not present for this SHA

Optimized hosted CI is enabled for this PR.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Hosted CI Requested

Triggered 9 workflow(s) for 3ad31b169592.
Mode: optimized hosted CI (path-selected by script/ci-changes-detector).
Added ready-for-hosted-ci, so future commits will keep running optimized hosted CI until +ci-stop-hosted is used.

View progress in the Actions tab.

@justin808
justin808 added this pull request to the merge queue Jul 3, 2026
Merged via the queue into main with commit 40e0ae9 Jul 3, 2026
75 checks passed
@justin808
justin808 deleted the codex/batch-e-pro-rsc-stream branch July 3, 2026 05:18
justin808 added a commit that referenced this pull request Jul 3, 2026
…derer-shutdown-restart

* origin/main:
  Improve release-finish dry-run fetch handling (#4441)
  Flush RSC payloads before incomplete HTML tails (#4379)
  Handle sync RSC route failures as fetch errors (#4393)
justin808 added a commit that referenced this pull request Jul 3, 2026
…cache-4317

* origin/main:
  Improve release-finish dry-run fetch handling (#4441)
  Flush RSC payloads before incomplete HTML tails (#4379)
justin808 added a commit that referenced this pull request Jul 3, 2026
…370' into codex/batch-e-loadable-stats-retry-4371

* origin/codex/batch-e-rsc-parser-flush-4370:
  Add cached static RSC helper and diagnostics (#4386)
  Fix Pro tag revalidation retry after delete failures (#4375)
  Fix node renderer graceful shutdown restarts (#4400)
  Improve release-finish dry-run fetch handling (#4441)
  Flush RSC payloads before incomplete HTML tails (#4379)
  Handle sync RSC route failures as fetch errors (#4393)
  Delete never-wired RenderRequest/JsCodeBuilder/RenderingStrategy layer (#4414) (#4437)
  Delegate deprecated base/ shims to capabilities/ instead of cloning (#4413) (#4436)
  Remove pre-monorepo node-renderer devDep baggage; consolidate test multipart builders (#4435)
  Preserve streaming LoadError during dependency failures (#4388)
  Wire eslint-rules RuleTester suite into a runner, CI, and knip (#4409) (#4433)
  Handle fire-and-forget RSCRoute retry failures (#4378)
justin808 added a commit that referenced this pull request Jul 3, 2026
…nsport

* origin/main:
  Unify Pro component cache fetch behavior (#4384)
  Replace chalk with picocolors in create-react-on-rails-app (#4411) (#4444)
  Fix Pro RSC stylesheet stats retry after read failures (#4401)
  [Pro] Reduce tag-index cache work during streaming (#4443)
  Warn on truncated Pro RSC parser streams (#4392)
  Report response-start send rejections (#4389)
  Add cached static RSC helper and diagnostics (#4386)
  Fix Pro tag revalidation retry after delete failures (#4375)
  Fix node renderer graceful shutdown restarts (#4400)
  Improve release-finish dry-run fetch handling (#4441)
  Flush RSC payloads before incomplete HTML tails (#4379)
justin808 added a commit that referenced this pull request Jul 4, 2026
…w-boundary

* origin/main: (26 commits)
  [Pro] Extract async props settled chunk writer (#4448)
  Fix incorrect defer_generated_component_packs = false migration guidance (#4451)
  Fix Pro renderer transport memory and reuse (#4394)
  Fix Pro RSC loadable stats retry visibility (#4447)
  Unify Pro component cache fetch behavior (#4384)
  Replace chalk with picocolors in create-react-on-rails-app (#4411) (#4444)
  Fix Pro RSC stylesheet stats retry after read failures (#4401)
  [Pro] Reduce tag-index cache work during streaming (#4443)
  Warn on truncated Pro RSC parser streams (#4392)
  Report response-start send rejections (#4389)
  Add cached static RSC helper and diagnostics (#4386)
  Fix Pro tag revalidation retry after delete failures (#4375)
  Fix node renderer graceful shutdown restarts (#4400)
  Improve release-finish dry-run fetch handling (#4441)
  Flush RSC payloads before incomplete HTML tails (#4379)
  Handle sync RSC route failures as fetch errors (#4393)
  Delete never-wired RenderRequest/JsCodeBuilder/RenderingStrategy layer (#4414) (#4437)
  Delegate deprecated base/ shims to capabilities/ instead of cloning (#4413) (#4436)
  Remove pre-monorepo node-renderer devDep baggage; consolidate test multipart builders (#4435)
  Preserve streaming LoadError during dependency failures (#4388)
  ...

# Conflicts:
#	CHANGELOG.md
#	packages/react-on-rails-pro/src/RSCProvider.tsx
#	packages/react-on-rails-pro/src/RSCRoute.tsx
#	packages/react-on-rails-pro/tests/boundedCacheProvider.client.test.tsx
#	packages/react-on-rails-pro/tests/getReactServerComponent.client.test.ts
justin808 added a commit that referenced this pull request Jul 5, 2026
## Summary

- Adds the missing Batch E changelog entry for the Pro RSC streaming
flush fix from #4379 / #4327.
- Leaves the existing current `main` changelog entries for #4392, #4401,
#4447, and the later #4372 follow-up via #4438 unchanged.
- Tracks the separate Batch E QA evidence gap in #4470.

## Validation

- `git diff --check`
- `pnpm exec prettier --check CHANGELOG.md`
- Ruby changelog structure check for trailing newline and duplicate
`[Unreleased]` headings

Hook caveat: local pre-commit/pre-push link checks are blocked before
link checking by Lychee 0.24.2 failing to parse `.lychee.toml` at
`include_fragments = false`; the commit and push used `--no-verify` to
avoid mixing that unrelated toolchain issue into this changelog PR.


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

## Summary by CodeRabbit

* **Documentation**
  * Updated the changelog with a new Pro fix entry.
* Noted an improvement to streamed payload handling so complete HTML
content is delivered sooner, while incomplete text and markup are
preserved for the next chunk.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-hosted-ci Run optimized hosted GitHub CI for this PR

Projects

None yet

1 participant