Skip to content

Preserve Next.js and Google Tag Manager rewrites in streamed pages - #1222

Open
ChristianPavilonis wants to merge 15 commits into
mainfrom
fix/1206-1208-script-composition
Open

ChristianPavilonis wants to merge 15 commits into
mainfrom
fix/1206-1208-script-composition

Conversation

@ChristianPavilonis

@ChristianPavilonis ChristianPavilonis commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Fix aborted App Router responses and invalid or duplicated Pages Router data by composing script rewrites instead of letting one integration overwrite another.
  • Preserve both Next.js origin-URL rewrites and first-party Google Tag Manager/Analytics URLs inside React Server Components flight data, with correct payload lengths.
  • Keep buffering bounded and response-local, preserve raw script source, and restore original data safely when a payload cannot be rewritten.
  • Reject malformed effective request hosts with 400 before inserting request-derived bytes into HTML or Flight source.

Changes

Core paths below are relative to crates/trusted-server-core/src/.

File Change
html_processor.rs Match selectors as before, compose every script stage, and emit each fragment once without escaping JavaScript as HTML text.
http_util.rs Validate the selected request host, retaining valid ports, IPv6, local hosts, and header precedence. Malformed hosts return a bad-request error without fallback or reflection.
publisher.rs Propagate host validation errors before publisher and page-bids URL rewriting.
proxy.rs Propagate validation errors when signing needs request metadata to determine the scheme.
integrations/mod.rs Document that Next.js must precede GTM so both stages observe the same fragment.
integrations/registry.rs Document script-only selectors and how replacement, suppression, and final-fragment flushing compose.
integrations/prebid.rs Adapt the test helper to fallible request metadata extraction.
integrations/google_tag_manager.rs Preserve fragment order with bounded response-local buffering and rewrite quoted flight URLs without damaging escaped queries.
integrations/nextjs/mod.rs Connect enabled Google Tag Manager rewriting to flight processing and add chunk, compression, ownership, and existing-fixture regressions.
integrations/nextjs/rsc.rs Apply both URL rewrites after reading original record boundaries, then recompute their decoded byte lengths.
integrations/nextjs/rsc_placeholders.rs Capture multiple payloads atomically, preserve unsafe fallback data, and allow normal bootstrap controls before later payloads.
integrations/nextjs/rsc_stream.rs Carry the optional flight transformation and add fallback checks that preserve original payloads and strict placeholder errors.
integrations/nextjs/script_rewriter.rs Mark Pages Router fragments so JSON strings cannot be mistaken for executable flight scripts.
integrations/nextjs/script_lexer.rs Use bounded lexical filtering to distinguish executable Flight calls from inert spellings in strings, comments, and regular expressions. This is not a full JavaScript parser.
test_support.rs Provide canned identity/gzip origin responses and shared assertions for complete adapter output, without changing the existing default fixture.
crates/trusted-server-adapter-axum/tests/routes.rs Verify complete rewritten 200 responses, malformed-host rejection with 400, and compatibility with local hosts, IPv6, and ports.
crates/trusted-server-adapter-fastly/src/app.rs Drain the lazy response stream completely and verify identity/gzip output, rewrites, and payload lengths.

Scope

These three issues share the same script-processing defect, so this PR fixes them together. The diff is mostly regression tests and test fixtures covering fragment boundaries, compression, multiple payloads, buffering limits, bootstrap scripts, and escaped queries. Runtime changes stay in the shared Rust processing path; the adapter changes are tests only.

There are no new settings, dependencies, integration-order changes, or adapter error-handling changes. Missing generated placeholders still fail rather than silently hiding a lost rewrite.

Retained boundaries:

  • Standalone Google Tag Manager rewriting can still miss a hostname split inside its initial six-byte marker prefix.
  • A hostname split across separate flight payload scripts is safely preserved rather than rewritten; input chunks within a captured payload are handled.
  • Unsupported or unsafe payloads retain their original data rather than risk corrupting hydration.

Deferred review discussion

  • Flight GTM rewriting applies to quoted or whole-span URLs, including bare origins and values used as rendered content. This differs from SSR HTML attribute and text rewriting and may cause hydration mismatches or visible-text changes. Restricting Flight rewrites to script-bearing values needs a separate policy discussion. Rejecting bare origins alone would not solve the full mismatch and could break scripts that append tracking paths later. No Flight-policy change is included in this review follow-up.
  • Performance optimization is deferred. This review follow-up adds no lexer, qualifier-tail, or GTM-transform optimization.
  • Host validation checks syntax, not whether a forwarded host comes from a trusted proxy. Forwarded-header precedence and adapter trust policy remain unchanged. Requests without host metadata retain their existing empty-host behavior.

Closes

Closes #1206
Closes #1207
Closes #1208

Test plan

Regression failures were recorded before their corresponding fixes, including lost placeholders, invalid JSON, duplicate/reordered fragments, bootstrap bypass, escaped-query misses, and raw-source escaping. The malformed-host adapter regression returns 200 on the original code and 400 after validation. The deferred Flight-policy concern is recorded above.

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npm exec --no -- vitest run using the installed Vitest without dependency installation.
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: the unchanged npm run format script passed with a command-local PATH to the existing Prettier 3.8.1, matching the docs lockfile; the ordinary command cannot resolve Prettier because docs-local dependencies are absent.
  • WASM release build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve; direct Viceroy client testing was used instead.
  • Other: native core tests, Cloudflare/Spin tests, CLI/browser fixtures, host adapter parity, all remaining target-matched lint aliases, and the JavaScript build passed.
  • Complete Axum and Fastly response bodies were checked for identity and gzip, including normal bootstrap, escaped queries, Unicode, both URL rewrites, and decoded payload lengths.
  • Actual local Viceroy clients received complete 200 responses for identity and gzip, with valid payload JSON, both rewrites, the final HTML suffix, and no generated placeholders.

Final counts include 2,883 native core tests, 14 adapter parity tests, and 1,155 JavaScript tests. Existing target-specific panic/doctest skips remain; native panic counterparts and all CLI browser fixtures ran. Local transport testing used debug WASM and a synthetic HTTP origin, not production TLS.

Checklist

  • Changes follow AGENTS.md conventions.
  • No unwrap() in new production code; use expect("should ...").
  • Uses the project's log macros rather than println!, as required by AGENTS.md.
  • New behavior has regression tests.
  • No secrets or credentials committed.

Apply matching script stages in sequence and emit each fragment once.
Keep GTM buffering response-local and bounded, and rewrite captured
Flight payloads only after reading their original record boundaries.
Preserve unsafe originals and recompute decoded record lengths.

Cover Pages and App Router data, bootstrap controls, escaped queries,
source-order fallback, and complete identity/gzip adapter responses.

Refs #1206, #1207, #1208
@ChristianPavilonis
ChristianPavilonis marked this pull request as ready for review October 1, 2026 12:06
@ChristianPavilonis
ChristianPavilonis requested review from aram356, dhruv8sh and prk-Jr and removed request for aram356 and prk-Jr October 1, 2026 12:07

@dhruv8sh dhruv8sh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Composes script stages instead of letting one integration's Replace overwrite another's, and threads GTM rewriting into captured Flight payloads with recomputed T lengths. The fallback paths are conservative and the boundary-sweep test coverage is excellent; no blocking issues found. A few design questions and minor cleanups below.

3 of the inline comments below carry a one-click GitHub suggestion — use Commit suggestion (or Add suggestion to batch for several at once) to apply them as commits on the PR branch. The remaining comments describe the fix in prose because the change touches lines outside the diff or needs an author decision.

Non-blocking

🤔 thinking / ⛏ nitpick

  • Flight GTM rewriting is broader than the HTML policy (bare origins, rendered content) — see inline at crates/trusted-server-core/src/integrations/google_tag_manager.rs:194
  • ContentType::Html removes the last escaping layer for request-host-derived bytes — see inline at crates/trusted-server-core/src/html_processor.rs:716
  • Script rewriter selectors are now effectively script-only — see inline at crates/trusted-server-core/src/html_processor.rs:682
  • Per-character work on every inline script — see inline at crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs:476
  • Redundant enabled filter — see inline at crates/trusted-server-core/src/integrations/nextjs/mod.rs:114
  • Assertions without messages — see inline at crates/trusted-server-core/src/integrations/google_tag_manager.rs:3606 and crates/trusted-server-core/src/html_processor.rs:838

Cross-cutting / body-level findings

  • 🏕 Registration order is now load-bearing but undocumented — GTM's protects_current_flight_fragment reads a snapshot the Next.js placeholder stage writes for the same fragment, so nextjs must stay ahead of google_tag_manager in builders() (crates/trusted-server-core/src/integrations/mod.rs). Likewise NextJsNextDataRewriter must precede NextJsRscPlaceholderRewriter, which consumes its one-shot next_data_fragment. A reorder would make GTM read the previous fragment's flag, and only the mixed integration tests would catch it. Suggest a comment alongside the existing "This must remain first" note (apply manually — mod.rs is outside the diff):

            // `nextjs` must precede `google_tag_manager`: GTM's script stage reads the
            // Flight-protection snapshot the Next.js stage records for the same fragment.
  • 📝 PR description misses the new file — the Changes table doesn't list the new nextjs/script_lexer.rs (318 lines, the lexical filter that decides executable vs inert __next_f spellings), and Scope says "11-file diff" while the diff is 12 files. Worth adding so reviewers know it exists and that it is intentionally "not a JavaScript parser".

  • 👍 Atomic capture and fallback discipline — batched capture in rsc_placeholders.rs discovers every range and validates the queue budget before committing anything, fallback always restores untouched originals (enabled_gtm_transform_never_mutates_fallback_originals locks that in), and the every-split-boundary sweeps plus identity/gzip end-to-end checks through both Axum and Fastly with decoded T-length assertions are exactly the right regression net for this class of streaming bug.

CI Status

  • integration tests (Fastly EC lifecycle): PASS
  • browser integration tests: PASS
  • integration tests: PASS
  • cargo test (cross-adapter parity): PASS
  • CodeQL: PASS
  • cargo test (ts CLI, native): PASS
  • format-docs: PASS (required)
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • format-typescript: PASS (required)
  • Analyze (rust): PASS
  • Analyze (javascript-typescript): PASS
  • prepare integration artifacts: PASS
  • vitest: PASS
  • cargo test (axum native): PASS
  • cargo fmt: PASS (required)
  • Analyze (javascript-typescript): PASS
  • cargo test: PASS (required)
  • CLAUDE.md symlink guard: PASS
  • Analyze (actions): PASS

Comment thread crates/trusted-server-core/src/integrations/google_tag_manager.rs Outdated
Comment thread crates/trusted-server-core/src/html_processor.rs Outdated
Comment thread crates/trusted-server-core/src/html_processor.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/nextjs/mod.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/google_tag_manager.rs Outdated
Comment thread crates/trusted-server-core/src/html_processor.rs Outdated
Keep regex literals after statement blocks inert without hiding Flight calls after object, function, or class expressions. Bound brace and pending-class context alongside existing lexical nesting limits.

Add fragment-boundary and identity/gzip regressions for origin/GTM rewrites and decoded Flight T lengths. Both reported brace/slash regressions were reproduced before the fix.

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Request changes for three reproduced P2 correctness issues:

  • A superclass function body confuses the new lexer, allowing GTM rewriting to produce an invalid Flight T-record byte length.
  • A trailing __next_f property reference disables rewriting for the current and subsequent Flight scripts.
  • An ordinary GTM URL before a Flight call is rewritten differently depending on input chunk boundaries.

The inline comments describe the reproductions and proposed remedies.

Validation: 2,876 existing native core tests passed, as did Axum tests, cargo clippy-axum, cargo fmt --all -- --check, and the focused Fastly complete-response regression under Viceroy. Additional scratch regression tests reproduced the issues above. GitHub CI was green at submission.

Reviewed commit: d738d93f70e3a1e79bfa9256cc730a317d14381a.

Comment thread crates/trusted-server-core/src/integrations/nextjs/script_lexer.rs
Comment thread crates/trusted-server-core/src/integrations/google_tag_manager.rs Outdated

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Re-reviewed the streamed Next.js/GTM composition and the host-validation follow-up at e81e5d98d7518836ac2aad3e80728785b535c9c4. Request changes: the three previously reproduced P2 correctness findings remain unresolved; the affected runtime code is unchanged from the earlier review, and the author confirms the follow-up commits do not address them.

Blocking

🔧 wrench

The existing inline threads contain the reproductions and proposed remedies. No duplicate inline comments or new findings are included in this re-review.

Validation

Compared the affected runtime code with the previously reviewed commit d738d93f70e3a1e79bfa9256cc730a317d14381a: the lexer and payload-discovery files are byte-identical, and the GTM change is an assertion message only. Reviewed the complete changed files and removed diff code with independent reviewers. Verified the remote CI results at the reviewed head, including all eight target-matched clippy steps; no full local CI rerun was performed.

CI Status

  • browser integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • integration tests: PASS
  • CodeQL: PASS
  • cargo test (ts CLI, native): PASS
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • cargo test (axum native): PASS
  • CLAUDE.md symlink guard: PASS
  • Analyze (javascript-typescript): PASS
  • cargo fmt: PASS (required)
  • Analyze (rust): PASS
  • format-typescript: PASS (required)
  • Analyze (javascript-typescript): PASS
  • cargo test: PASS (required)
  • format-docs: PASS (required)
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo test (cross-adapter parity): PASS
  • prepare integration artifacts: PASS
  • vitest: PASS
  • Analyze (actions): PASS

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Re-reviewed at b3538b47d. 4d73741 resolves the three P2s from the previous round. I checked each fix against its regression test: the superclass lexer state, trailing __next_f references, and the chunk-invariant ordinary GTM before captured Flight. One correctness issue remains in the same family as the first P2. A lexer false negative still lets GTM corrupt raw Flight T lengths, because protection depends only on the masked-code view. Requesting changes for a fail-closed guard instead of more per-spelling lexer fixes.

Blocking

🔧 wrench

  • Lexer false negatives fail open into invalid Flight T lengths. Five valid-JS prefixes reproduce it. A raw-qualifier protection fallback was prototyped locally and fixes all five. See inline at crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs:435.

Non-blocking

🤔 thinking

  • Underscore host labels now return 400. See inline at crates/trusted-server-core/src/http_util.rs:242.

Cross-cutting / body-level findings

  • 📌 GTM without Next.js still corrupts App Router Flight lengths. With nextjs disabled and GTM enabled, <script>self.__next_f.push([1,"1:T64,{\"tag\":\"http://www.googletagmanager.com/gtm.js?id=…\"…}"])</script> is rewritten to declare 0x64 (100) for a 101-byte body. protects_current_flight_fragment returns false when no Next.js state exists. By code reading, this path is unchanged from main, so it is out of scope here. It is worth a follow-up issue, since GTM-only deployments on App Router sites hit it with no Next.js involvement.
  • 👍 Prior-round fixes are well locked in. trailing_next_f_references_preserve_current_and_later_flight, ordinary_gtm_before_captured_flight_is_chunk_invariant and the expanded superclass sweep cover every source split plus identity/gzip. The Suppress → unprotected change in rewrite_claimed_fragment fixes chunk variance without touching GTM's raw fallback restoration.

Local validation (PR head b3538b47d)

  • cargo test -p trusted-server-core --lib --target aarch64-apple-darwin: 2,946 passed
  • cargo test-fastly (Viceroy): pass (core 2,940 passed / 6 ignored; adapter 196 passed)
  • cargo test-axum: pass
  • cargo clippy-fastly: pass
  • cargo fmt --all -- --check: pass

CI Status

All 19 checks pass, including cargo fmt, cargo test, axum, cloudflare, spin, the parity, CLI and integration suites, vitest, format-docs, format-typescript and CodeQL.

Comment thread crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs Outdated
Comment thread crates/trusted-server-core/src/http_util.rs Outdated

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Approving. Script stages now compose instead of overwriting each other, ContentType::Html stops entity-escaping JS, GTM state is per-document, and the raw push-head observer from af69781 closes the fail-open lexer path from round 3. I found no fail-open path in Next.js-emitted spellings. The earlier round-1 and round-3 points are resolved or explicitly deferred.

Verified locally at 514a617: core lib tests (2949 passed), cargo test-spin, cargo test-axum, Fastly adapter under Viceroy (196 passed), and clippy-fastly / clippy-axum / clippy-spin-native / clippy-spin-wasm all clean.

CI note: 7 jobs (integration, browser integration, spin, format-typescript, CodeQL rust/python/js-ts) were cancelled with "job was not acquired by Runner", which is an infra failure, not code. Please re-run them before merge.

Non-blocking follow-ups

  • 🤔 Inline-script cost with Next.js enabled (rsc_placeholders.rs ~L434/441/540). This adds numbers to the deferred perf thread. Release build, native arm64, 8 KB chunks, Next.js + GTM: a 2.1 MB non-Flight inline script goes from ~2 ms (base 8048301) to ~36 ms (~18×), and a 1.7 MB Flight push goes from ~8 ms to ~20 ms. WASM will be slower. A cheap pre-filter would avoid most of it: only mask and observe when the fragment plus a short carried tail contains __next_f, or when a head is in progress. Batched trimming instead of a per-char drain would also help. Worth a tracked issue if it stays deferred.
  • 🤔 Host validation applies to every proxied request (publisher.rs ~L4443, http_util.rs ~L185), including assets that never reach a script rewriter. An empty X-Forwarded-Host, a Forwarded value that is not visible ASCII, or a valueless host= now returns 400 instead of falling back to a lower-priority header. Low impact on Fastly and Spin, which strip these headers, but notable for Axum or Cloudflare behind a proxy. Either document it as intended or tolerate an unparseable Forwarded value that has no host=.
  • ⛏ Redundant RequestInfo::from_request in handle_page_bids (publisher.rs ~L6985). It duplicates the extraction ~30 lines earlier, and the first binding is still live.
  • ⛏ About 40 new assert!/assert_eq! calls lack a "should …" message (AGENTS.md). Examples: adapter-axum/tests/routes.rs:1079, adapter-fastly/src/app.rs:3191, test_support.rs, and nextjs/mod.rs.
  • 📝 For the deferred Flight GTM policy discussion: rewrite_gtm_rsc_span rewrites quoted analytics.google.com URLs and bare origins without any GTM marker. The ordinary script path deliberately doesn't treat that domain as a trigger.

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Re-reviewed at 514a617a0. The composition model, per-document GTM state, raw ContentType::Html emission and host validation hold up. Two new correctness bugs remain in the Flight path: a lexer miss can still leave invalid T lengths when the row continues in the next script (Next.js alone, a regression from main), and GTM's whole-span Flight routing can produce http:/integrations/… for a URL split after its scheme. The branch also conflicts with main, and the required format-typescript check never ran.

3 of the inline comments below carry a one-click GitHub suggestion. Use Commit suggestion (or Add suggestion to batch for several at once) to apply them as commits on the PR branch. The remaining comments describe the fix in prose because the change touches multiple files or lines outside the diff and can't be auto-applied.

Blocking

🔧 wrench

  • A Flight push the lexer misses still corrupts T lengths when its row continues in the next script: see inline at crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs:475
  • Whole-span routing half-rewrites a URL split after its scheme across Flight payload scripts: see inline at crates/trusted-server-core/src/integrations/google_tag_manager.rs:204
  • Conflicts with main: see cross-cutting below
  • Required CI never ran: see cross-cutting below

Non-blocking

🤔 thinking / ♻️ refactor / ⛏ nitpick

  • Non-null form state and binary pushes disable Flight rewriting for the rest of the page: see inline at crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs:40
  • Skip the script handlers when no script rewriter is registered: see inline at crates/trusted-server-core/src/html_processor.rs:665
  • Domain-lint markers for PR-added test hosts: see inline at crates/trusted-server-core/src/integrations/google_tag_manager.rs:3636 and crates/trusted-server-core/src/integrations/nextjs/mod.rs:1216

Cross-cutting / body-level findings

  • 🔧 Conflicts with main. google_tag_manager.rs conflicts with #1179, which moved GTM's buffer to a per-document ScriptTextAccumulator (registry.rs, re-exported from integrations/mod.rs). A resolution that keeps this PR's GtmScriptState checks out locally:

    • Take the PR side of both hunks (the struct and the top of rewrite). Main's comment above get_or_insert_with, its // allow-domain: comment and its new an_interrupted_document_leaves_no_residue_for_the_next_document test merge cleanly.
    • Restore Mutex in use std::sync::{Arc, LazyLock, Mutex}; (the auto-merge drops it) and drop the now-unused use crate::integrations::ScriptTextAccumulator; (clippy -D warnings fails on it).
    • Delete ScriptTextAccumulator from registry.rs and its re-export from integrations/mod.rs. After the merge it is public API with no users, and the deletion passes fmt and clippy.

    On that merged tree, cargo fmt --check, clippy-fastly, clippy-axum, test-axum, test-fastly and test-fastly-reuse pass, as do 2,956 native core tests, template_build_digest (no fixture update needed: the build digest rotates automatically) and both of main's new regression tests against this PR's logic.

  • 🔧 Required CI never ran. Seven jobs were cancelled with "The job was not acquired by Runner of type hosted even after multiple attempts" and never re-run: format-typescript (required), integration tests, browser integration tests, cargo check/build/test (spin native + wasm32-wasip1), Analyze (rust), Analyze (javascript-typescript) and Analyze (python). The ruleset requires the branch to be up to date with main, so the merge above will start a fresh run; please make sure these go green.

  • 📌 The ordinary GTM script path has the same scheme-split problem, and it predates this PR. rewrite_gtm_urls (L515-537, called at L1182) tries GTM_WHOLE_URL_PATTERN, which is meant for attribute values, on held script text that can begin at any chunk boundary. A split just after the scheme gives j.src='http:/integrations/google_tag_manager/gtm.js?id='+i+dl, and a split at the opening quote leaves later URLs in the script unrewritten. Main behaves the same, so this is follow-up material: a quoted-only helper for the script path fixes it (the URL is then left unrewritten instead of corrupted). Relatedly, the retained boundary in the PR description ("a hostname split inside its initial six-byte marker prefix") understates the window: every split from just after the opening quote through googl misses, about 17 positions for "https://www.googletagmanager.com.

  • 🌱 GTM and Next.js now depend on each other. GTM calls nextjs::protects_current_flight_fragment (google_tag_manager.rs:1125), nextjs/mod.rs reads GoogleTagManagerConfig and takes rewrite_gtm_rsc_span, and correctness depends on builders() order. A third integration that needs Flight protection or a Flight transform would add more cross-imports. A neutral home would fit better as a follow-up: a registry-owned "current fragment holds raw Flight" flag on IntegrationScriptContext, and a span-transform hook such as IntegrationRegistration::with_flight_span_transform that GTM registers.

  • 🤔 The lexer keeps producing this bug class. The superclass case, the five round-3 prefixes and the continuation case above all come from script_lexer.rs misclassifying publisher code that shares a <script> with a Flight push. Next.js never emits that: each push is its own script. An alternative that removes most of the 513-line lexer is to capture only scripts whose whole content is a sequence of Next.js-shaped pushes, and to treat any other script with a raw qualified data head as unsupported (bypass_rsc), as main did for unsupported pushes. The trade-off is losing rewriting on pages where publisher code and a push share a script. Worth weighing before the next lexer fix.

  • 🏕 Docs and changelog. docs/guide/integrations/google_tag_manager.md ("1. Script Rewriting") doesn't mention that GTM/GA URLs inside App Router Flight payloads are now rewritten with recomputed T lengths, or that this depends on nextjs being enabled. No guide mentions the new 400 for a malformed effective host; main's docs/guide/ts-dev-proxy.md:291 says core "prefers X-Forwarded-Host, then Host", omitting Forwarded and the 400. docs/guide/creative-processing.md:752 still says remove_node() deletes the script element, which contradicts the new ScriptRewriteAction::RemoveNode doc in registry.rs. CHANGELOG.md has no [Unreleased] entry.

  • 🏕 Leftover function-local imports. crates/trusted-server-adapter-axum/tests/routes.rs:883-885 still has use std::sync::Arc; and use trusted_server_core::test_support::nextjs_auction; inside nextjs_auction_output_holds_until_the_structural_body_close, now that the PR hoists both to module level. AGENTS.md asks for no local imports.

  • ♻️ Transform plumbing. Option<fn(&str) -> String> now runs through eight signatures (rewrite_span, both rewrite functions, the factory and the processor), and the fix for the whole-span bug pushes rewrite_span to seven arguments, the AGENTS.md limit. A small SpanRewrite { rewriter, origin_host, request_host, request_scheme, transform } with fn apply(&self, span, record_text) would carry it. The single-payload loop (rsc.rs ~L465-513) and the combined loop (~L715-763) differ only in the length function and could share one helper, and rewrite_rsc_scripts_combined_with_limit now only forwards None.

Lower-priority notes (each verified, all minor):

  • 🤔 html_processor.rs:667-679: the flat match-flag vector is reset at every <script> start tag and never restored at its end tag. Inside <svg> a <script> can nest, so the outer script's remaining text runs with the inner script's flags (<svg><script>a<script id="target">b</script>c</script></svg> feeds "c" to a script#target stage). Negligible in practice; worth a doc line.
  • 📝 http_util.rs:214-255 accepts any single DNS label, including script. With that host, bare-host Flight rewriting (host_rewrite.rs) turns an origin-supplied raw </origin.example.com> into </script>. Not a regression (main had no validation, and Next.js never emits a raw < in Flight), but it qualifies the reasoning that validation makes raw emission safe. Not treating < or </ before the host as a boundary would close it.
  • 🤔 http_util.rs:202-203: an empty Host: now returns 400, while an absent Host still yields an empty host. RFC 9112 sends an empty Host when the target has no authority, so the two cases mean the same thing. It only affects Axum or proxy setups.
  • 📝 rsc.rs:223 (unchanged by this PR): the unescaped-length model miscounts \xe9, \0, octal escapes, \q, line continuations and \u{…}. Next.js's JSON.stringify plus htmlEscapeJsonString never emits them, and the GTM transform can't introduce them, so nothing is needed here.
  • 📝 google_tag_manager.rs:1147-1159: the limit counts held bytes plus the current fragment, so a single final fragment larger than max_buffered_script_bytes now passes through unrewritten, where main rewrote it. Unreachable at the defaults (16 MiB limit, 8 KB chunks); a one-line comment would make the intent clear.
  • ⛏ google_tag_manager.rs:451-455: GtmScriptState and its fields have no doc comments, and the release blocks at L1129-1134 and L1154-1159 are identical. An impl GtmScriptState { fn release(&mut self, content: &str) -> ScriptRewriteAction } would remove the duplication.
  • ⛏ script_lexer.rs: MAX_TEMPLATE_DEPTH (L6) also caps braces, parentheses and pending classes, so MAX_LEXICAL_DEPTH would read better; self.word.len() < 10 (L130) is a magic number; ScriptLexer (L56) and its expression_start / statement_start invariants are undocumented. In rsc_placeholders.rs, slicing code at offsets taken from content relies on mask emitting one # per masked byte; a debug_assert_eq!(code.len(), content.len()) would document it.
  • ⛏ rsc.rs:626 (mostly pre-existing): the classifier compares payload bytes with the limit, but the combined rewriter adds 7 marker bytes per boundary, so a group within 7 * (n - 1) bytes of the limit is classified rewritable and then skipped with two warn! lines. Removing the origin-host early exit for GTM-enabled groups makes this more reachable.

CI Status

  • integration tests (Fastly EC lifecycle): PASS
  • integration tests: CANCELLED
  • browser integration tests: CANCELLED
  • CodeQL: SKIPPED
  • Analyze (actions): PASS
  • Analyze (javascript-typescript): PASS
  • cargo test (ts CLI, native) (ubuntu-latest): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native) (macos-latest): PASS
  • prepare integration artifacts: PASS
  • format-docs: PASS (required)
  • cargo test (axum native): PASS
  • CLAUDE.md symlink guard: PASS
  • cargo test: PASS (required)
  • cargo fmt: PASS (required)
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • vitest: PASS
  • cargo check/build/test (spin native + wasm32-wasip1): CANCELLED
  • format-typescript: CANCELLED (required)
  • Analyze (rust): CANCELLED
  • Analyze (javascript-typescript): CANCELLED
  • Analyze (python): CANCELLED

Comment thread crates/trusted-server-core/src/integrations/google_tag_manager.rs Outdated
Comment thread crates/trusted-server-core/src/html_processor.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/google_tag_manager.rs Outdated
Comment thread crates/trusted-server-core/src/integrations/nextjs/mod.rs Outdated
@aram356 aram356 added this to the 202610 milestone Oct 12, 2026
aram356 added a commit that referenced this pull request Oct 12, 2026
Brings #1222 at 514a617.

Integration decisions:
- RequestInfo::from_request keeps #1251's source order (authenticated
  forwarder, immutable ingress, runtime origin, Host) and applies
  #1222's host syntax check to whichever host is selected. It returns
  BadRequest for a malformed or non-text selected host. Raw Forwarded
  and X-Forwarded-Host are never selected in the RC, so #1222's tests
  for those headers now assert they are ignored instead of rejected.
- RC-only callers (DataDome protection, Fastly compat, Spin, forwarder,
  orchestrator and publisher tests) handle the fallible signature.
  DataDome only reads RequestInfo behind an authenticated origin and
  keeps its request_host fallback.
- The first-party sign path prefers the authenticated forwarder scheme
  (#1251) and propagates #1222's host error.
- The GTM rewriter takes #1222's per-document GtmScriptState, which
  supersedes main's ScriptTextAccumulator use from #1179; state still
  lives in IntegrationDocumentState, and #1179's two-document residue
  tests remain.
@aram356

aram356 commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

@ChristianPavilonis I don't see resolution for the previous review. Can you please address with comment and resolve it? Thanks.

aram356 added a commit that referenced this pull request Oct 15, 2026
ChristianPavilonis and others added 2 commits October 16, 2026 15:53
Resolve merge conflicts preserving streamed rewrites and base regressions; exclude build-only git metadata from legacy settings parsing.

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Summary

Re-reviewed at 6b317b558. Every round-1 blocker is resolved, and each fix was checked against the original repro: the lexer-miss continuation now fails closed with consistent T lengths, a Flight URL split after its scheme is preserved (no bad T header across about 100k sweep cases), non-null form state no longer disables rewriting, the handler guard and lint markers are applied, the branch is up to date with main, and all 22 checks pass.

Requesting changes for what is still open: how far an inert data-head hint should reach, the settings change that came in through a merge commit, the round-1 leftovers (ScriptTextAccumulator, the local imports, docs, changelog and PR description) and a test gap on the new record_text sites.

3 of the inline comments below carry a one-click GitHub suggestion. Use Commit suggestion (or Add suggestion to batch for several at once) to apply them as commits on the PR branch. The remaining comments describe the fix in prose because the change touches multiple files or lines outside the diff and can't be auto-applied.

Requested before merge

  • 🤔 Inert self.__next_f.push([1, text in any script disables all later Flight rewriting: see inline at crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs:35
  • ♻️ Settings change from merge commit d0cfd88 duplicates open PR #1326: see inline at crates/trusted-server-core/src/settings.rs:3057
  • ♻️ ScriptTextAccumulator is now dead public API: see inline at crates/trusted-server-core/src/integrations/registry.rs:575
  • ⛏ Function-local imports still present: see inline at crates/trusted-server-adapter-axum/tests/routes.rs:10
  • 🏕 Docs, changelog and PR description: see inline at crates/trusted-server-core/src/integrations/registry.rs:69
  • 🏕 Pin every record_text = false site: see inline at crates/trusted-server-core/src/integrations/nextjs/mod.rs:1322

Other findings

  • 🤔 The document-wide Flight fallback is silent: see inline at crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs:586
  • ⛏ Restore main's comment that the merge dropped: see inline at crates/trusted-server-core/src/integrations/google_tag_manager.rs:1135
  • ⛏ Say why a combined record body is still safe to route: see inline at crates/trusted-server-core/src/integrations/google_tag_manager.rs:194
  • ♻️ Observer state sprawl: see inline at crates/trusted-server-core/src/integrations/nextjs/rsc_stream.rs:73
  • ⛏ Oversized pass-through branch is unreachable through lol_html: see inline at crates/trusted-server-core/src/integrations/nextjs/rsc_placeholders.rs:258
  • 📌 A removed <script src> with an inline Flight push fails the whole response (pre-existing): see inline at crates/trusted-server-core/src/html_processor.rs:684
  • 📌 An unanchored T-header lookalike can leave a stale length (pre-existing): see inline at crates/trusted-server-core/src/integrations/nextjs/rsc.rs:478

Lower-priority notes:

  • 📝 A comment or BOM inside a data head (push(/*c*/[1, or push(\u{FEFF}[1,) still yields a stale T length, but only when the lexer also misses that push (for example after foo: { bar() } /'/.test(s);). Without such a prefix this head handles both, [1.0, and [0x1, correctly, and main is stale on the comment spelling even without a prefix, so this is not a regression.
  • ⛏ The 22ef7ca07 subject is 103 characters and ends with a period (CONTRIBUTING.md asks for about 50 and no period). A squash merge makes this moot.

Round-1 verification

Round-1 finding Status at 6b317b558
Lexer miss leaves a stale T length when the row continues in the next script Fixed, fail-closed: all five prefixes give consistent lengths at chunk 16, 32 and 8192, Next.js only and mixed; the regression test fails when the bypass assignment is removed
Whole-span routing half-rewrites a URL split after its scheme Fixed for the reported shape; see the test-gap comment for the other three span sites
Non-null form state disables later rewriting Fixed; binary channel 3 keeps the document fallback by design, as documented
Script handlers registered with zero rewriters Applied, byte-identical to the suggestion
Domain-lint markers Applied; ts dev lint domains --changed-vs a9037770a reports 0 violations
Conflict with main Resolved; up to date with main, main's #1179 regression tests pass
Required CI never ran Resolved; all checks pass

CI Status

  • browser integration tests: PASS
  • integration tests (Fastly EC lifecycle): PASS
  • integration tests: PASS
  • CodeQL: PASS
  • cargo test (ts CLI, native) (macos-latest): PASS
  • Analyze (rust): PASS
  • Analyze (python): PASS
  • cargo test (axum native): PASS
  • CLAUDE.md symlink guard: PASS
  • cargo fmt: PASS (required)
  • Analyze (javascript-typescript): PASS
  • cargo test: PASS (required)
  • format-typescript: PASS (required)
  • cargo check (cloudflare native + wasm32-unknown-unknown): PASS
  • Analyze (actions): PASS
  • format-docs: PASS (required)
  • cargo check/build/test (spin native + wasm32-wasip1): PASS
  • cargo test (cross-adapter parity): PASS
  • cargo test (ts CLI, native) (ubuntu-latest): PASS
  • prepare integration artifacts: PASS
  • vitest: PASS
  • Analyze (javascript-typescript): PASS


// A missed data push may open a T row that a later captured script continues.
// Generic push hints still protect only their current script from ordinary GTM.
static QUALIFIED_FLIGHT_DATA_HEAD: LazyLock<Regex> = LazyLock::new(|| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤔 thinking: Inert self.__next_f.push([1, text in any script now disables all later Flight rewriting

This pattern runs on raw text, so a data-head spelling inside a string, comment, template literal or JSON-LD body of an unrelated script reaches the end-of-script bypass (L586) and turns off origin and GTM rewriting for every later Flight payload on the page. With a large input chunk it also restores earlier payloads still queued in that chunk. Each document below is an earlier Flight push, then the hint script, then a later push carrying origin and GTM URLs (*_origin=true means the origin URL was rewritten):

hint                     chunk  head 6b317b558                            main a9037770a (Next.js only)
plain string             8192   earlier_origin=false later_origin=false   earlier_origin=true later_origin=true
plain string             16     earlier_origin=true  later_origin=false   earlier_origin=true later_origin=true
line or block comment    8192   earlier_origin=false later_origin=false   earlier_origin=true later_origin=true
single quotes, template  8192   earlier_origin=false later_origin=false   earlier_origin=true later_origin=true
JSON-LD articleBody      8192   earlier_origin=false later_origin=false   earlier_origin=true later_origin=true

Generic push( hints, a captured push whose payload contains the text, form state and __NEXT_DATA__ do not trigger it. window.app.__next_f=1;self.__next_f.push([1,…]) in one script does: rewrite_fragment releases the whole fragment when the first __next_f has a foreign receiver (L403-421), and that release now escalates through this guard.

This costs coverage, never correctness, and our round-1 sketch proposed keying on exactly this text, so the trade-off is partly ours. The realistic trigger is a page whose content quotes the Flight push (developer blogs, docs). Two ways forward:

  • (a) Accept it: pin it with a regression test (a JSON-LD hint followed by a later push, asserting the later push stays raw with consistent T lengths) and state it in the PR description.
  • (b) Narrow it: escalate an uncaptured raw data head to the whole document only when the lexer ends the script outside clean code state, and keep per-script protection otherwise. In a probe, all five round-1 lexer-miss prefixes end non-clean and every hint case ends clean. The caveat is a contrived double trap that ends clean while masking a real push, which this head handles today.

Either way, picking the first __next_f with a qualified receiver in rewrite_fragment would remove the foreign-reference trigger. Two related edges: binary [3,…] has the same queued-restore chunk dependence (worth one sentence in the L605-608 doc), and form state nested deeper than the lexer's 32-level limit that also contains push text is bypassed by the opaque branch (L527) before rewrite_complete can recognize it.

if ctx.is_last_in_text_node {
// Raw data can continue a row in the next script. Keep the entire
// remaining Flight stream raw, not merely this GTM snapshot.
state.bypass_rsc |= state.raw_data_seen || state.raw_data_unclaimed;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤔 thinking: The document-wide Flight fallback is silent

When this line (or the opaque branch at L527, or the unsupported-push path) flips bypass_rsc, every later Flight payload on the page loses origin and GTM rewriting, yet rsc_placeholders.rs contains no log:: call. Every other Flight fallback logs log::warn!("Next.js RSC fallback: …") in rsc_stream.rs (L346, L384, L394, L401, L424, L561), and release_bypass only warns when a group is held. One log::warn! when bypass_rsc first goes from false to true would tell operators that a page lost Flight rewriting, and why.

}
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🏕 camp site: Pin every record_text = false site, not just one

flight_url_split_after_scheme_preserves_the_whole_destination covers the single-payload before span (rsc.rs L458). Flipping any of the other three false sites to true (L474: a continuation that precedes a T row; L706: one grouped with the next payload by a trailing hex run; L724: one before a T body that spans the next payload) brings back http:/integrations/google_tag_manager/..., and all 3,013 existing native core tests still pass. The test below covers all three plus the complete-record case. Locally it passes at this head and fails under each of the three mutations:

left:  "0:{\"href\":\"https://test.example.com/p\",\"tag\":\"http:/integrations/google_tag_manager/gtm.js?id=GTM-MIX1\"}\n1:T5,hello"
right: "0:{\"href\":\"https://test.example.com/p\",\"tag\":\"http://www.googletagmanager.com/gtm.js?id=GTM-MIX1\"}\n1:T5,hello"
Suggested change
}
}
#[test]
fn flight_url_split_after_scheme_is_preserved_at_every_span_site() {
for scheme in ["http:", "https:"] {
let body = json!({
"tag": format!("{scheme}//www.googletagmanager.com/gtm.js?id=GTM-MIX1"),
"href": "https://origin.example.com/p",
});
let payload = format!("0:{body}\n");
let split = payload
.find("//www.")
.expect("should locate the tag slashes");
let (head, continuation) = payload.split_at(split);
// The continuation precedes a T row, is grouped by a trailing hex run,
// or precedes a T row whose body spans the next payload.
for parts in [
vec![head.to_owned(), format!("{continuation}1:T5,hello")],
vec![
head.to_owned(),
format!("{continuation}2:\"ab"),
"cd\"\n".to_owned(),
],
vec![
head.to_owned(),
format!("{continuation}1:T5,he"),
"llo".to_owned(),
],
] {
let refs: Vec<&str> = parts.iter().map(String::as_str).collect();
let expected = parts
.concat()
.replace("origin.example.com", "test.example.com");
for chunk_size in [16, 8192] {
let payloads = flight_payloads(&mixed_output(&flight_html(&refs), chunk_size));
assert_eq!(
payloads.len(),
parts.len(),
"should retain payload boundaries"
);
assert_eq!(
payloads.concat(),
expected,
"should preserve a GTM URL split after its scheme at every span site"
);
}
}
}
let url = "http://www.googletagmanager.com/gtm.js?id=GTM-MIX1";
let record = format!("1:T{:x},{url}", url.len());
let split = record.len() - "MIX1".len();
let payloads = flight_payloads(&mixed_output(
&flight_html(&[&record[..split], &record[split..]]),
8192,
));
let routed = "/integrations/google_tag_manager/gtm.js?id=GTM-MIX1";
assert_eq!(
payloads.concat(),
format!("1:T{:x},{routed}", routed.len()),
"should route a whole T-record URL whose query spans payload scripts"
);
}

// Per document, never per registry: a registry-lifetime buffer would
// carry one document's partial script into the next.
let accumulator = ctx
let state = ctx

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⛏ nitpick: Restore main's comment that the merge dropped

The conflict resolution in d0cfd88 dropped main's (#1179) comment explaining why this state is per document. Our round-1 note said that comment would merge cleanly, which was wrong: it sat inside the conflict hunk.

Suggested change
let state = ctx
// Per document, never per registry: a registry-lifetime buffer would
// carry one document's partial script into the next.
let state = ctx

Comment on lines +194 to +195
/// `record_text` marks exactly one complete T-record body. Other spans can start
/// mid-URL at a payload-script boundary and must not be routed as whole URLs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⛏ nitpick: Say why a combined record body is still safe to route

In the combined path a complete T-record body can contain RSC_MARKER, i.e. a payload boundary inside a URL. That is safe because a marker inside the routed prefix (scheme, slashes, host, path) prevents a match, and one in the suffix is copied verbatim; a sweep over URLs split at every position inside a spanning T body confirmed both. Worth stating in the doc:

Suggested change
/// `record_text` marks exactly one complete T-record body. Other spans can start
/// mid-URL at a payload-script boundary and must not be routed as whole URLs.
/// `record_text` marks exactly one complete T-record body. Other spans can start
/// mid-URL at a payload-script boundary and must not be routed as whole URLs.
/// A record body may still span payloads: a boundary marker inside the routed
/// prefix prevents routing, and one in the suffix is copied verbatim.

Keep,
/// Pass replacement text to subsequent matching stages.
Replace(String),
/// Suppress this fragment; downstream stages still run and may flush held text.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🏕 camp site: Docs, changelog and PR description

Round 1's docs items are still open, and the description has drifted since 22ef7ca:

  • docs/guide/integrations/google_tag_manager.md ("1. Script Rewriting") doesn't mention that GTM/GA URLs inside App Router Flight payloads are now rewritten with recomputed T lengths, or that this depends on nextjs being enabled.
  • No guide mentions the new 400 for a malformed effective host; docs/guide/ts-dev-proxy.md:291 still says core "prefers X-Forwarded-Host, then Host".
  • docs/guide/creative-processing.md:752 says remove_node() deletes the script element, which contradicts the RemoveNode doc here.
  • CHANGELOG.md has no [Unreleased] entry.
  • PR description: "Unsupported or unsafe payloads retain their original data" reads as per payload, but an uncaptured data head, an inert data-head hint (see the comment on rsc_placeholders.rs:35) and binary [3,…] now keep the rest of the document raw. The newly inert channel-2 form state and settings.rs are missing, the test counts are stale (3,013 native core tests pass at this head), and main's Require specs for feature PRs #1385 template now asks for a ## Spec line ("Not a feature" for a fix).

}
let script_patterns = patterns.clone();
let script_document_state = document_state.clone();
element_content_handlers.push(text!("script", move |text| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📌 out of scope: A removed <script src> with an inline Flight push fails the whole response (pre-existing)

When an attribute rewriter removes a script element (the Prebid integration does this for publisher Prebid scripts), lol_html still runs this text handler on the removed element's content. If that body is a Flight push, Next.js captures it and queues a placeholder, lol_html drops the output, and the next placeholder reaches the stream processor out of order:

<script src="https://cdn.example.com/prebid.js">self.__next_f.push([1,"0:{\"url\":\"https://origin.example.com/a\"}\n"])</script>
<script>…normal Flight push…</script>

Next.js + GTM and Next.js only: ERR(Next.js RSC output contains an unknown generated placeholder),
for the whole document and all 174 two-way splits

Main behaves identically, browsers ignore the inline body of a src script, and Next.js never emits this shape, so it is not for this PR. Worth a follow-up issue: skipping the composed stages for a removed element would close it.

let chunk_content = &content[chunk.header_end..chunk.content_end];
let rewritten_content =
rewriter.rewrite_to_string(chunk_content, origin_host, request_host, request_scheme);
let rewritten_content = rewrite_span(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📌 out of scope: An unanchored T-header lookalike can leave a stale length (pre-existing, now also for GTM URLs)

TCHUNK_PATTERN (L11) matches anywhere, so a lookalike inside a JSON string swallows the real T header that follows it, and a rewrite inside the swallowed range leaves that header stale. Decoded input 0:["see 1:T41,"]\n2:T2e,{"u":"http://www.googletagmanager.com/gtm.js"}3:"xxxx…"\n comes out as 2:T2e,{"u":"/integrations/google_tag_manager/gtm.js"} (46 bytes declared, 47 actual). The same scan already does this to origin-host rewrites on main (39 declared, 37 actual for an origin URL), so this PR extends an existing failure rather than introducing it. Follow-up material: anchor T-header discovery to row starts.

pub(super) raw_data_seen: bool,
/// Data observed in released source cannot be accounted for by a later buffer.
pub(super) raw_data_unclaimed: bool,
pub(super) raw_data_tail: String,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

♻️ refactor: Observer state sprawl

NextJsDocumentState now carries three parallel observer groups (flight_qualifier_tail/flight_node_owned, raw_qualifier_tail/raw_flight_seen/raw_flight_unclaimed, raw_data_tail/raw_data_seen/raw_data_unclaimed), reset field by field in rsc_placeholders.rs (L476-486, L586-599, with partial clears at L190 and L303). A HeadObservation { tail, seen, unclaimed } with a reset() would make a missed field impossible. Smaller cleanups in the same area: the same slices are scanned once per regex (L116-119, L170-173, L180, L198-200), the if was_buffered { replace } else { Keep } return arm repeats five times, and the 2\s*,\s*null branch of INERT_FLIGHT_CONTROL_ARGUMENTS is now covered by the serde form-state path.

if !state.flight_node_owned {
return ScriptRewriteAction::Keep;
}
if is_last && content.len() > limit && content.contains("__next_f") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⛏ nitpick: This oversized pass-through branch is unreachable through lol_html

lol_html delivers every non-empty script as non-last fragments followed by an empty last fragment, so Idle && is_last && content.len() > limit only happens in unit tests; oversized scripts take BypassUntilLast instead. The branch's head_start also comes from the first qualified push rather than the captured call, so a raw data head between the two would not be scanned. A comment marking it as defensive, or slicing the data check at the captured call's start, would make that explicit.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

4 participants