Conversation
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
The parser-aware seam paths are well covered, but the new RSC streaming implementation introduces CPU-scaling, false-positive matching, and chunk-dependent fallback behavior that should be corrected before merging.
[P3] Update the deprecation guidance to name an available replacement (crates/trusted-server-core/src/integrations/nextjs/html_post_process.rs:98)
The public functions are still re-exported for compatibility, but their documentation and compiler deprecation notes tell callers to use NextJsHtmlPostProcessor, which this PR deletes.
Please point callers to the enabled Next.js streaming integration, or state that these compatibility functions have no direct public replacement.
aram356
left a comment
There was a problem hiding this comment.
Summary
Parser-confirmed body seam and bounded Next.js streaming, verified by execution rather than inspection: with the Next.js processor registered a non-final chunk emits real output pre-EOF, and a false </body> literal inside RSC data now streams past while the auction is still pending. BodyCloseHoldBuffer and every auction-coordination scan for </body are gone, and all 19 CI checks pass.
Two findings block. The RSC_PUSH_CALL_PATTERN widening changes behaviour for scripts that are not Next.js RSC at all, and the T-chunk escape scanner panics on publisher-controlled input reachable through the new classification path. Neither comment carries a one-click suggestion: I applied the obvious regex narrowing in a scratch worktree and it broke html_processor_rewrites_rsc_stream_payload_with_chunked_input, so the fix is described in prose instead of proposed as bytes I could not stand behind.
Blocking
🔧 wrench
- Widened RSC push pattern rewrites unrelated publisher scripts — see inline at
crates/trusted-server-core/src/integrations/nextjs/shared.rs:21 - Escape scanner panics on non-char-boundary indices — see inline at
crates/trusted-server-core/src/integrations/nextjs/rsc.rs:210
Non-blocking
🤔 thinking
- Deferred seam token is minted by construction but claimed by convention — see inline at
crates/trusted-server-core/src/publisher.rs:650 release_bypassrequires every captured placeholder in the current chunk — see inline atcrates/trusted-server-core/src/integrations/nextjs/rsc_stream.rs:346
♻️ refactor
- Parser-side classification passes
usize::MAXinstead of the configured bound — see inline atcrates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs:114
⛏ nitpick
- Parity test shares one auction counter across adapters — see inline at
crates/trusted-server-integration-tests/tests/parity.rs:1112 - Parity test uses Axum as its own oracle — see inline at
crates/trusted-server-integration-tests/tests/parity.rs:1176
Cross-cutting / body-level findings
-
🏕
html_post_process.rsis dead surface, and the PR description overstates its removal. The description says the whole-document post-processor was removed; 231 lines went, but 674 remain.post_process_rsc_htmlandpost_process_rsc_html_in_placehave zero callers anywhere incrates/outside this module's own tests (verified by workspace grep) — roughly 150 lines of production code including a second fulllol_htmlre-parse infind_rsc_push_scripts, plus ~400 lines of test module, all compiled into the wasm build behind#[allow(deprecated)]. Their doc comments still point atNextJsHtmlPostProcessor, which this PR deletes, so the surviving documentation describes a type that no longer exists. There are no callers left to migrate, so CLAUDE.md's "migrate callers and delete legacy APIs" applies cleanly. Deleting the module and themod.rsre-export would drop ~674 lines of wasm-compiled dead weight; at minimum the stale doc comments need fixing. -
📝 The adapter regressions landed in a different crate than the plan specifies, leaving three CI gates blind to this path. The plan (
docs/superpowers/plans/2026-09-07-850-parser-aware-body-hold-nextjs-streaming.md:839-891, Task 7 Steps 5 & 7) prescribes "one buffered route regression in each adapter test module", listscrates/trusted-server-adapter-{axum,cloudflare,spin}/tests/routes.rsunder Modify, and gives per-adapter verification commands. None of those three files changed —git diff --name-onlyshows zerotests/routes.rsin this PR. The single fixture lives only incrates/trusted-server-integration-tests/tests/parity.rs. Consequence:cargo test-axum,cargo test-cloudflare, andcargo test-spin(CI gate 3) do not exercise the Next.js/auction path at all, and the newpub fn routes_with_settings_and_servicesadded to all three adapters is dead code within its own crate — its only consumer is the parity suite. The plan's completion review does disclose the relocation, but the Step 5/7 recipe and the Modify list were never corrected, so the plan still claims coverage that does not exist. Either move a fixture into each adapter's own tests, or amend the plan and note explicitly that per-adapter gates do not cover this path. -
📌 Escape-scanner panic is pre-existing on
main, not introduced here — flagged because this PR widens its reach. I verifiedEscapeSequenceIteris byte-identical at the merge base and that the same payload reached it there throughrewrite_rsc_scripts_combined_with_limit, so this is not a regression and is legitimately out of scope for a fix in this PR. It is called out because the newclassify_rsc_groupcall site runs the scanner on borrowed publisher fragments inside alol_htmlcallback, which is a materially wider and earlier exposure than the old post-processor path. A follow-up issue is the right home if you would rather not grow this PR.
CI Status
- integration tests: PASS
- browser integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- CodeQL: PASS
- cargo test: PASS (required)
- cargo test (ts CLI, native): PASS
- vitest: PASS
- format-typescript: PASS (required)
- Analyze (javascript-typescript): PASS
- cargo test (axum native): PASS
- Analyze (rust): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo test (cross-adapter parity): PASS
- cargo fmt: PASS (required)
- prepare integration artifacts: PASS
- Analyze (actions): PASS
- format-docs: PASS (required)
Address review feedback on the parser-aware body hold. Require a qualified `self.`/`window.` receiver again. The widened pattern matched `myAnalytics.__next_f.push([1,"url"])` in unrelated publisher scripts and rewrote their string literals. The bare form was load-bearing because the receiver can stream away before `__next_f` is recognised, so the receiver is now retained in a bounded context on the document state and verified out of band; an anchored pattern is used only once that context proves it. Reject a match whose preceding byte continues a member expression. Keep the escape scanner on character boundaries. `\x` advanced four bytes without checking, so publisher input such as `1:T9,\x4é` panicked the HTML pipeline and returned 500. Only the boundary is validated, not the hex digits, so the unescaped byte count that drives T-chunk length recomputation is unchanged for input that previously scanned. Classify a group incrementally. Rebuilding and rescanning the whole group per payload cost 2.4s for an 8.96MiB T-chunk over 256 payloads; feeding each payload once costs 19ms, and the cost no longer scales with segment count. Chunk content consumption and non-chunk segment inspection both resume where they stopped, and EOF reclassifies with nothing held back. Scope the captured-payload limits. `max_combined_payload_bytes` no longer bounds the parser queue in aggregate, so two independent payloads sharing a source chunk stop bypassing each other; the queue is bounded by the parser's own script-buffer budget and a payload count instead. Return the deferred inline seam token from `PublisherBodyProcessor::new` so a caller without a seam controller discards it visibly, and log when one is dropped, since a debug assertion is compiled out of the wasm builds that ship. Delete `html_post_process.rs`. It had no callers outside its own tests and documented a type this work removes. Share the Next.js auction fixture through `test-utils` so each adapter carries its own buffered regression and the parity suite stops duplicating it. Assert on the recomputed Flight `T` length rather than a substring the split fixture can never contain.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
Reviewed 481b24f7de7d7e6e3bb55d7fe59297383a2f95e1 against 6cae7f5da8911c746cf873581885f90c3820dd96. The parser-confirmed auction seam and bounded Next.js RSC streaming paths are covered by focused WASM tests across supported encodings, and all reported CI checks pass. No actionable issues found.
aram356
left a comment
There was a problem hiding this comment.
Summary
Second review pass, against head 481b24f7d. Every finding from the previous pass is addressed, and I re-verified the two blocking ones by running the original repros rather than reading the diff: all four regex false positives now return None with every legitimate receiver form still matching, and all three previously-panicking escape inputs now scan cleanly. The seam-token constructor change is a better fix than the one proposed. html_post_process.rs is fully deleted, each adapter now carries its own buffered regression that genuinely runs under its own CI alias, and the plan doc records each deviation instead of silently dropping it. I also reproduced the classifier performance claim independently: 8.75 MiB across 256 segments classifies in 26ms.
The hardening commit introduced two new defects, both in the fixes themselves. Neither corrupts a response — both degrade to the hydration-safe fallback — but the first one undoes the streaming behaviour this PR exists to deliver, so it should not merge as-is.
One of the inline comments carries a one-click suggestion; it passed the full CI-equivalent gate in isolation (cargo fmt, clippy-fastly, clippy-axum, test-fastly, test-axum, test-cloudflare, test-spin, and the cross-adapter parity suite), with a byte-exact drift check confirming the posted bytes are the bytes that were tested. The other blocking finding is prose-only on purpose: its fix interacts with a length-counting invariant described in that comment, and proposing bytes I had not proven safe would be worse than describing the constraint.
Blocking
🔧 wrench
- Escape hold-back defers every realistic Flight payload, defeating streaming — see inline at
crates/trusted-server-core/src/integrations/nextjs/rsc.rs:302 - Over-limit trimmed claim always forces document-wide RSC bypass — see inline at
crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs:132(carries a suggestion)
Non-blocking
🤔 thinking
- Incremental classifier can latch
Invalidwhere a full rescan reportsNeedMore— see inline atcrates/trusted-server-core/src/integrations/nextjs/rsc_stream.rs:1224
⛏ nitpick
- Split-invariance test compares the classifier against itself — see inline at
crates/trusted-server-core/src/integrations/nextjs/rsc_stream.rs:1224 - Broken intra-doc link to a symbol that does not exist — see inline at
crates/trusted-server-core/src/integrations/nextjs/shared.rs:18 - New public context fields carry no doc comments — see inline at
crates/trusted-server-core/src/integrations/registry.rs:557
Cross-cutting / body-level findings
-
🌱 Lazy Fastly disconnect still records no abandonment telemetry, and this PR is not the cause. A client disconnect mid-stream on the lazy path leaves
DispatchedAuctionGuard::droplogging alog::warn!with no terminalabandonedrow, so the SSP fan-out and its quota consumption vanish from telemetry. The write-sink driver now handles this correctly via the newabandon_hold_auction(..., "stream_write_error")wrapper. I want to be precise about scope, because the asymmetry invites the wrong conclusion:origin/maincarries the byte-identical warn-onlyDropimpl, andstream_write_errordoes not appear anywhere onmain. So this PR added abandonment telemetry to one driver and left the other exactly as it found it. That is a net improvement, not a regression, and it should not gate this PR. Worth a follow-up issue, with the note that a generator drop is not anErr, so the write-sink'sis_err()branch will not port across directly: it needs a guard-side terminal emit or an explicit drop hook. -
📝 Worst-case concurrent RSC memory now compounds across four buffers. The captured-payload queue bound moved from
max_combined_payload_bytes(10 MiB default) toctx.max_buffered_script_bytes(publisher.max_buffered_body_bytes, 16 MiB default), andRscGroupClassifiernow retainscombinedfor the lifetime of a group, which is a second copy of bytes already held inself.group(the previousclassify_rsc_groupbuilt and dropped that string per call). Worst case, concurrently live: roughly 16 MiB queue + 10 MiBheld_output+ 10 MiBcombined+ 10 MiBgroup. I cannot demonstrate an OOM, and each individual bound is defensible, but the two changes compound and the total is worth confirming against the Fastly Compute heap budget rather than discovering at peak traffic. -
📌
migration_guards.rs's allowlist is hand-maintained and silently incomplete. Out of scope here and not caused by this PR, which correctly adds its one new core file (rsc_stream.rs) and removes the deleted one. Flagging because the guard's value depends on the list: roughly two dozen core modules are absent from it (auction/plan.rs,config.rs,platform/template_cache.rs,price_bucket.rs, among others), so a future file added without a list entry is unguarded with no failing test to say so. A directory-walk-based guard would remove the maintenance burden. None of the missing modules were touched by this PR.
CI Status
All 20 checks reported by gh pr checks are PASS, including the four required gates. I also re-ran cargo fmt --all -- --check and cargo clippy-fastly against a clean export of this head, both clean, and cargo test -p trusted-server-core --lib reports 2702 passed / 0 failed.
- integration tests: PASS
- browser integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- CodeQL: PASS
- cargo test: PASS (required)
- cargo test (ts CLI, native): PASS
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- vitest: PASS
- cargo fmt: PASS (required)
- format-typescript: PASS (required)
- format-docs: PASS (required)
- CLAUDE.md symlink guard: PASS
- Analyze (rust): PASS
- Analyze (javascript-typescript): PASS
- Analyze (actions): PASS
- prepare integration artifacts: PASS
Summary
</body>tail, preventing inline JavaScript or JSON literals from holding most of the page.Changes
crates/trusted-server-core/src/html_processor.rscrates/trusted-server-core/src/integrations/google_tag_manager.rscrates/trusted-server-core/src/integrations/mod.rscrates/trusted-server-core/src/integrations/nextjs/html_post_process.rscrates/trusted-server-core/src/integrations/nextjs/mod.rscrates/trusted-server-core/src/integrations/nextjs/rsc.rscrates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rscrates/trusted-server-core/src/integrations/nextjs/rsc_stream.rscrates/trusted-server-core/src/integrations/nextjs/script_rewriter.rscrates/trusted-server-core/src/integrations/nextjs/shared.rscrates/trusted-server-core/src/integrations/registry.rscrates/trusted-server-core/src/publisher.rsdocs/guide/integrations/nextjs.mddocs/superpowers/specs/2026-09-07-850-parser-aware-body-hold-nextjs-streaming-design.mddocs/superpowers/plans/2026-09-07-850-parser-aware-body-hold-nextjs-streaming.mdCloses
Closes #850
Test plan
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest run(blocked by the existing CommonJS/ESM incompatibility betweenhtml-encoding-snifferand@exodus/bytes; no JS files changed)cd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute servecargo test-cloudflare,cargo test-spin,cargo clippy-cloudflare,cargo clippy-cloudflare-wasm,cargo clippy-spin-native, andcargo clippy-spin-wasmChecklist
unwrap()in production code — useexpect("should ...")println!)