Skip to content

Use raw bodies to avoid render request escaping#4579

Draft
alexeyr-ci2 wants to merge 5 commits into
mainfrom
alexeyr/3584/raw-rendering-request
Draft

Use raw bodies to avoid render request escaping#4579
alexeyr-ci2 wants to merge 5 commits into
mainfrom
alexeyr/3584/raw-rendering-request

Conversation

@alexeyr-ci2

@alexeyr-ci2 alexeyr-ci2 commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fix #3584.

Pull Request checklist

  • Add/update test to cover these changes
    - [ ] Update documentation
  • Update CHANGELOG file

Agent Decision Log

Continuation commit b62f91b (review closeout) made these non-blocking decisions:

  • Non-blocking: Rolling-deploy compatibility after @fastify/formbody removal (review thread)
    • Decision: Keep formbody registered this release; the renderer accepts both the legacy urlencoded and the new raw content type. PROTOCOL_VERSION stays 2.0.0.
    • Why: With the protocol version unchanged, an old gem against an upgraded renderer renders normally through the parsed-body path; removing the parser would turn documented version-drift handling into an unactionable 415. A PROTOCOL_VERSION bump would not help either drift direction, since an old renderer fails on the new content type before any version check.
    • Review later: Drop formbody together with the next PROTOCOL_VERSION bump.
  • Non-blocking: CodeQL js/reflected-xss (1 high) at the render response send
    • Decision: Unified setResponse so every non-stream string response carries explicit text/plain; charset=utf-8 + X-Content-Type-Options: nosniff (previously only the >= 400 branch did).
    • Why: Render results and error text embed request-derived content; the Rails client reads raw body text, so forcing a non-HTML content type is behavior-preserving — no existing ResponseResult sets its own Content-Type.
    • Review later: None.
  • Non-blocking: react_on_rails_pro/Gemfile.lock BUNDLED WITH 2.5.4 → 4.0.10
    • Decision: Reverted to 2.5.4 (main's value).
    • Why: Incidental stamp from a newer local Bundler; the repo toolchain pins 2.5.4. (The review-bot claim that Bundler 4.x does not exist is stale — the revert is for toolchain parity, not that reason.)
    • Review later: None.

Confidence note: raw-path worker tests 60/60 locally (including new 401-precedence and legacy urlencoded round-trip tests); Pro request/http-client specs 104 examples 0 failures; rubocop, RBS, tsc, eslint, prettier clean. Streaming jest suites fail/hang locally on a clean checkout too (environmental); hosted CI is the arbiter for those. Net package.json + pnpm-lock.yaml diff vs main is zero after the formbody restore.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8193ebd6-f67a-49e8-bb66-a5b37790457e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch alexeyr/3584/raw-rendering-request

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 11, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 63.51 KB (0%)
react-on-rails/client bundled (gzip) (time) 63.51 KB (0%)
react-on-rails/client bundled (brotli) 54.46 KB (0%)
react-on-rails/client bundled (brotli) (time) 54.46 KB (0%)
react-on-rails-pro/client bundled (gzip) 64.87 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 64.87 KB (0%)
react-on-rails-pro/client bundled (brotli) 55.81 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 55.81 KB (0%)
registerServerComponent/client bundled (gzip) 135.37 KB (0%)
registerServerComponent/client bundled (gzip) (time) 135.37 KB (0%)
registerServerComponent/client bundled (brotli) 81.66 KB (0%)
registerServerComponent/client bundled (brotli) (time) 81.66 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 127.84 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 127.84 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 74.96 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 74.96 KB (0%)

Comment thread react_on_rails_pro/Gemfile.lock Outdated
Comment thread react_on_rails_pro/lib/react_on_rails_pro/request.rb
Comment thread packages/react-on-rails-pro-node-renderer/src/worker.ts
@claude

claude Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review: Use raw bodies to avoid render request escaping (#4579)

Overview

This PR replaces application/x-www-form-urlencoded bodies for non-bundle render requests with a new application/vnd.react-on-rails.render-request+javascript content type: the JS renderingRequest is sent verbatim as the raw HTTP body, and metadata (protocolVersion, gemVersion, railsEnv, password as a Bearer token, dependencyBundleTimestamps, rscStreamObservability) moves into x-react-on-rails-pro-* headers. This avoids URL-encoding/escaping the JS source, which is presumably the root cause of #3584. The multipart path (bundle uploads) is unchanged; @fastify/formbody is dropped since url-encoded bodies are no longer produced by the gem for renders.

Good coverage on the new code paths: normalizeRawRenderRequest (Node) and raw_render_request/request_body (Ruby) both get dedicated unit tests, including a header-injection regression test (\r\n in the password) and a malformed-JSON-header rejection test, plus an end-to-end worker test round-tripping quotes/unicode through the raw body.

Issues found (posted inline)

  1. react_on_rails_pro/Gemfile.lockBUNDLED WITH jumps from 2.5.4 to 4.0.10, a Bundler major version that doesn't appear to exist. Looks like accidental output from a local bundle install/lock run rather than an intended change; likely to break bundle install for anyone/CI without that exact Bundler version.
  2. request.rb#raw_render_request — only the password value is run through sanitize_raw_render_header (CRLF check) before being placed in a header; protocolVersion, gemVersion, railsEnv, and the JSON-encoded dependencyBundleTimestamps are not. All are currently server/gem-controlled so there's no live injection today, but the asymmetric treatment is fragile given the whole point of that helper is closing a header-injection path. Also flagged: this method, common_form_data, and the Node-side normalizeRawRenderRequest must be kept in sync by hand — since non-bundle renders now default to this raw path, a future field added to common_form_data without updating raw_render_request will raise ArgumentError on every non-bundle SSR call in production.
  3. worker.ts render route — for the raw content-type branch, header-shape validation (normalizeRawRenderRequest) runs and can 400 before performRequestPrechecks (password/protocol check) runs, so malformed-but-unauthenticated requests skip password verification while well-formed-but-wrong-password requests hit the 401 path. Low severity given this is described as an internal, non-internet-facing service, but it's inconsistent with /asset-exists, which authenticates first.

Not flagged inline — needs attention

benchmarks/bench-node-renderer.rb is not part of this diff but is broken by it. It posts render requests with Content-Type: application/x-www-form-urlencoded directly to the node renderer in two places:

  • rsc_bundle? (line ~108, via curl) — used to categorize bundles as RSC/non-RSC before benchmarking.
  • run_vegeta_benchmark (line ~383) — the actual Vegeta load-test target file for the whole benchmark suite.

Since @fastify/formbody is removed and no content-type parser is registered for application/x-www-form-urlencoded anymore, Fastify will reject these requests (415) rather than parsing them — rsc_bundle? will silently return nil for every bundle, and the main Vegeta benchmark will hit render errors instead of exercising the render path. Given this PR carries the benchmark label (presumably to run the benchmark CI job), this looks like it would break that very job. The script should be updated to use the new raw content type (or JSON) to match the gem's new request format.

Minor

  • The PR checklist has "Update CHANGELOG file" unchecked — this is a protocol-level behavior change (new content type, dropped dependency) worth a changelog entry for react_on_rails_pro.
  • RAW_RENDER_CONTENT_TYPE / RAW_RENDER_HEADER_PREFIX are duplicated as independent string literals in worker.ts and request.rb; low risk given they're gated behind protocolVersion, but there's no single source of truth tying the Ruby and JS sides together.

No correctness issues found in the core encode/decode round-trip logic itself (header parsing, boolean/array field handling, and the dependencyBundleTimestamps empty-vs-undefined edge case all check out against downstream usage).

@alexeyr-ci2
alexeyr-ci2 force-pushed the alexeyr/3584/raw-rendering-request branch from a54ebae to ce32f4b Compare July 12, 2026 08:28
@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review

What this PR does: Fixes #3584 by switching the render-request body from application/x-www-form-urlencoded to a raw JS body (custom content type application/vnd.react-on-rails.render-request+javascript) with metadata carried in X-React-On-Rails-Pro-* headers instead of form fields. This avoids escaping bugs where the rendering-request JS contains characters that don't round-trip cleanly through URL-encoding. Bundle-upload requests (send_bundle: true) keep using multipart form data. @fastify/formbody is dropped entirely from the node-renderer.

Overall the implementation is careful — header-injection is guarded against for the password (sanitize_raw_render_header rejects CR/LF), the new normalizeRawRenderRequest validates dependency-timestamp JSON and the observability flag, and there's solid new test coverage on both the Ruby and TS sides (request_spec.rb, renderer_http_client_spec.rb, worker.test.ts, bench_node_renderer_spec.rb).

Main concern: breaking wire-protocol change with no version gate, contradicting documented rolling-deploy guidance

This repo explicitly documents the Node Renderer as a standalone service that can be deployed independently from Rails (docs/oss/building-features/node-renderer/container-deployment.md, "Separate Workloads" topology). That doc calls out exactly this scenario:

Version drift risk — During rolling deploys, Rails and the Node Renderer may briefly run different versions... The React on Rails gem and Node Renderer package have a protocol version. If they mismatch, render requests return an error. To avoid this: deploy both workloads simultaneously.

The promise there is that a version mismatch produces a clean, actionable error — and indeed checkProtocolVersionHandler.ts returns a helpful 412 ("Update either the renderer or the Rails server") when protocolVersion in the body doesn't match. But that check only runs after the body has been successfully parsed.

This PR removes @fastify/formbody entirely with no fallback, and the Ruby client always sends the new raw content type for non-bundle renders. So during a rolling deploy of a "Separate Workloads" setup:

  • Old gem → new renderer: sends application/x-www-form-urlencoded; the new renderer has no parser registered for it anymore → Fastify unsupported-media-type failure, not the documented 412.
  • New gem → old renderer: sends the new raw content type; the old renderer doesn't recognize it and has no renderingRequest field in a parseable body → hard failure, not the documented 412.

Every prior protocol version bump could still rely on both ends spearing the same wire encoding (form-urlencoded), so mismatches always reached the protocol-version check. This PR is the first change to break the wire encoding itself, silently invalidating the "you'll get a clear error" guarantee the deployment docs promise for that topology — and PROTOCOL_VERSION (react_on_rails_pro/lib/react_on_rails_pro/version.rb) wasn't bumped, nor was CHANGELOG.md updated (the PR checklist has that box unchecked) to warn operators that gem and node-renderer now must be deployed atomically.

Suggested fixes (any one of these):

  1. Keep @fastify/formbody registered as a fallback parser for one deprecation cycle, so an old-format request from a lagging gem version still parses far enough to hit the protocol-version check.
  2. Bump PROTOCOL_VERSION for this change and explicitly document in the CHANGELOG that gem + node-renderer must be deployed together for "Separate Workloads" setups (and cross-link to container-deployment.md).
  3. At minimum, update container-deployment.md's "Different versions after deploy" section to reflect that a transport-level mismatch (not just a protocol-version body mismatch) can now occur, since the error users see during drift will no longer be the friendly 412.

Minor notes

  • raw_render_request (react_on_rails_pro/lib/react_on_rails_pro/request.rb) only CRLF-sanitizes the password header value. protocolVersion/gemVersion/railsEnv come from trusted internal constants so this is low risk, but if any of those ever become configurable/user-influenced, they'd bypass the same guard — worth a comment noting the assumption, or routing all header values through sanitize_raw_render_header for consistency.
  • build_form_body now always multipart-encodes (build_multipart_body) even when the previous url-encoded branch would have been cheaper for pure key/value forms. In practice this looks benign since every caller that hits form: already includes a bundle file, but worth double-checking there's no metadata-only form: caller left that would now pay unnecessary multipart overhead.

Nice test coverage on the new raw-body path, and the CRLF-injection guard on the password header is a good catch. The main ask is closing the version-skew gap described above before this ships, given it's a documented supported deployment topology.

Comment thread packages/react-on-rails-pro-node-renderer/src/worker.ts
Comment thread react_on_rails_pro/lib/react_on_rails_pro/request.rb
…rmbody compat

- Run protocol/password prechecks before raw header shape validation so a
  malformed but unauthenticated raw request fails auth (401) first, matching
  the parsed-body path and /asset-exists
- Keep @fastify/formbody registered (with test) so a not-yet-upgraded gem in a
  rolling deploy still reaches the protocol/render path instead of an
  unactionable 415; package.json and pnpm-lock.yaml now match main
- Sanitize every raw render metadata header value against CRLF injection, not
  just password, and document the three-way field-list sync requirement
- Send every non-stream string response with explicit text/plain + nosniff,
  clearing the CodeQL js/reflected-xss alert on the render response path
- Revert the incidental Gemfile.lock BUNDLED WITH bump (2.5.4 stays)
- Add the [Pro] CHANGELOG entry with the renderer-first deploy-order note

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@justin808

Copy link
Copy Markdown
Member

+ci-run-hosted

@github-actions github-actions Bot added the ready-for-hosted-ci Run optimized hosted GitHub CI for this PR label Jul 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hosted CI Requested

Triggered 9 workflow(s) for b62f91be56a0.
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.

Comment thread packages/react-on-rails-pro-node-renderer/src/worker.ts
@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review: Use raw bodies to avoid render request escaping

Overview

Replaces the application/x-www-form-urlencoded body for non-bundle render requests with a raw JavaScript body (application/vnd.react-on-rails.render-request+javascript) plus X-React-On-Rails-Pro-* metadata headers, removing URL-encoding overhead on the (often large) renderingRequest payload. Bundle-upload requests keep using multipart. The renderer still parses legacy form-encoded bodies for rolling-deploy compatibility with not-yet-upgraded gems. Good test coverage added on both the Ruby client (request_spec.rb, renderer_http_client_spec.rb) and the node renderer (worker.test.ts), plus the benchmark script and CHANGELOG were updated to match.

Bug: dev-mode gem/renderer version-mismatch check is skipped for raw requests

rawRenderPrecheckBody (worker.ts ~L507-513) builds a precheck object with only protocolVersion and password, then calls performRequestPrechecks on it (~L663) before the rest of the raw body is normalized. checkProtocolVersionHandler.ts's checkProtocolVersion only runs its gem/renderer version-mismatch check (which raises a blocking 412 outside production) if (gemVersion) — and gemVersion is never included in the raw precheck body, even though it's available on req.headers at that point and is read a few lines later in normalizeRawRenderRequest. Since the raw path is now used for essentially every non-bundle render request, this dev-mode safety net is effectively disabled for the new default path. Left an inline comment with the specific fix suggestion (include gemVersion/railsEnv in the precheck body) and a note that none of the new tests cover a version-mismatch scenario over the raw path.

Other notes (non-blocking)

  • setResponse in worker.ts was simplified to always force Content-Type: text/plain + X-Content-Type-Options: nosniff for any string data, regardless of status code, whereas the old code only did this unconditionally for status >= 400 and otherwise deferred to any Content-Type already present in ResponseResult.headers. I confirmed no current call site relies on a custom Content-Type surviving for string response data, so this isn't an active bug today — just a latent foot-gun if a future success-path response tries to set a non-text/plain Content-Type alongside string data.
  • Security: the new sanitize_raw_render_header in request.rb correctly rejects \r\n in any metadata value before it becomes a header (tested for both the password and non-password cases — good header-injection coverage). Authorization parsing on the renderer side correctly requires the Bearer prefix before treating the remainder as the password.
  • RendererHttpClient#request_body correctly guards against callers passing more than one of form:/json:/raw: simultaneously, with a test for the ambiguous case.
  • Nice cleanup: removing flatten_url_encoded_form/url_encode and the now-dead hasHeader/setStringResponseHeaders helpers.

Overall this is a solid, well-tested change; the main thing worth fixing before merge is the version-mismatch precheck gap.

rawRenderPrecheckBody omitted the fields checkProtocolVersion uses for the
non-production gem/renderer version-mismatch 412, silently skipping that
guard on the raw path (the common non-bundle render path). Forward both
headers and add a test asserting a mismatched gem version still 412s
outside production.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review: Use raw bodies to avoid render request escaping (#4579)

Overview

Replaces the application/x-www-form-urlencoded body used for non-bundle render requests with a raw application/vnd.react-on-rails.render-request+javascript body plus X-React-On-Rails-Pro-* metadata headers, avoiding URL-encoding overhead on large rendering payloads. The Node renderer keeps @fastify/formbody registered so a not-yet-upgraded gem still works during a rolling deploy (protocol version stays 2.0.0). Bundle uploads keep using multipart. Good test coverage on both the Ruby (request_spec.rb, renderer_http_client_spec.rb) and TS (worker.test.ts) sides, including header-injection and auth-precedence tests.

What I verified

  • Field parity between common_form_data/form_with_code (Ruby) and normalizeRawRenderRequest/rawRenderPrecheckBody (Node) — matches, and raw_render_request defensively raises on any unmapped field.
  • Header-injection guard (sanitize_raw_render_header blocking \r/\n) is applied to every header value, including the password.
  • Auth-before-shape-validation ordering is intentional and tested (rawRenderPrecheckBody runs before normalizeRawRenderRequest's stricter checks).
  • build_form_body always using multipart now is safe: the only two callers of form: (/upload-assets, bundle-carrying render_code) always include at least one file part already.
  • No behavior change for dependencyBundleTimestamps/rscStreamObservability handling downstream (extractBodyArrayField/parseOptionalBooleanField accept the normalized raw-path shapes).

Findings (see inline comments)

  1. Diagnostic accuracy regression in worker.ts's rawRenderPrecheckBody: it always defines protocolVersion/gemVersion/railsEnv as object keys (even when the header wasn't sent), which corrupts the "MISSING (received fields: ...)" debug message used to diagnose exactly the rolling-deploy/version-mismatch scenarios this PR is designed around.
  2. Stale RBS signature for RendererHttpClient#post (missing the new raw: kwarg) and build_form_body's return type (still advertises the removed String/url-encoded branch). rbs:validate won't catch this since it only checks internal consistency, not conformance with the Ruby source.
  3. Latent footgun in setResponse: it now unconditionally overwrites Content-Type for any string response body, even though ResponseResult['headers'] still types 'Content-Type' as a supported field. Currently no ResponseResult sets a custom Content-Type, so this isn't an active bug, but it silently makes that part of the type dead code for future contributors.

Minor / non-blocking nits (not inline-commented)

  • benchmarks/bench-node-renderer.rb's render_body is now just an identity function (rendering_request) — could be inlined at call sites, though keeping it is harmless.
  • The raw content-type match req.headers['content-type']?.split(';', 1)[0] === RAW_RENDER_CONTENT_TYPE is case-sensitive; fine given both ends are internally controlled, but worth a comment if that assumption ever changes.

Overall the design is sound, well-tested, and the rolling-deploy compatibility tradeoffs are clearly documented in the PR description. The three findings above are all fixable in isolation and none are blocking from a correctness/security standpoint of the core protocol change.

Comment thread packages/react-on-rails-pro-node-renderer/src/worker.ts
Comment thread packages/react-on-rails-pro-node-renderer/src/worker.ts
… at the type level

- rawRenderPrecheckBody no longer defines keys for absent headers, so the
  protocol-version 412's "received fields" list only names fields the client
  actually sent; test added for the no-metadata case
- ResponseResult marks Content-Type as never: setResponse intentionally forces
  text/plain on string payloads (reflected-XSS hardening), so a future caller
  setting a custom Content-Type now fails to compile instead of being silently
  clobbered

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review: PR #4579 — Use raw bodies to avoid render request escaping

Overview

Switches non-bundle render requests from application/x-www-form-urlencoded to a raw application/vnd.react-on-rails.render-request+javascript body (JS code verbatim) with request metadata (protocolVersion, gemVersion, railsEnv, password, dependencyBundleTimestamps, rscStreamObservability) carried in X-React-On-Rails-Pro-* headers / Authorization: Bearer. The Node renderer keeps @fastify/formbody registered so older gems still work during rolling deploys. Also unifies setResponse so every string response gets text/plain; charset=utf-8 + nosniff (fixes a CodeQL reflected-XSS finding along the way), and simplifies build_form_body to always use multipart (dropping the now-unused url-encoded branch).

Strengths

  • Header-injection hardening: sanitize_raw_render_header rejects \r/\n in every metadata value before it becomes a header, with dedicated specs for both the password and non-password paths (request_spec.rb).
  • Auth-before-shape ordering is correct and tested: rawRenderPrecheckBody runs checkProtocolVersion/authenticate using only the raw headers before normalizeRawRenderRequest does any shape validation (e.g. malformed dependency-bundle-timestamps JSON), so an unauthenticated malformed request gets 401, not a shape-validation 400/412. Traced checkProtocolVersion/authenticate's field usage against rawRenderPrecheckBody — the field lists match exactly, and it's covered by the "authenticates before validating header shape" test.
  • Backward compatibility is real, not just claimed: traced the retry path (send_bundle: true still goes through the untouched multipart form: path) and the RSC streaming path (render_code_as_stream always uses send_bundle: false for the render call itself, matching pre-existing behavior) — both are unaffected by the new raw-body branch, and this is backed by the "preserves the form path for bundle uploads" spec.
  • ResponseResult['Content-Type']: never is a nice type-level guard ensuring no caller can bypass the centralized content-type hardening in setResponse; confirmed no other code in src/ sets it via the ResponseResult interface (only the direct res.header(...) call inside setResponse itself).
  • Good symmetric test coverage across both sides of the protocol (Node renderer worker.test.ts, Ruby request_spec.rb/renderer_http_client_spec.rb, and the benchmark script/spec), including the legacy-urlencoded round-trip test.

Minor observations (non-blocking)

  1. raw_render_request(js_code, form) redundant parameter (react_on_rails_pro/lib/react_on_rails_pro/request.rb): js_code is always identical to form["renderingRequest"] (set by form_with_code). Passing both is a little redundant — could just read it from form — but it's harmless and arguably more explicit/defensive.
  2. Content-Type matching is exact-string, not case-insensitive (worker.ts, the req.headers['content-type']?.split(';', 1)[0] === RAW_RENDER_CONTENT_TYPE check): fine today since only this codebase's own Ruby client sets the header, but if a proxy/CDN ever normalizes casing of the media type, requests would silently fall into the "Invalid or missing request body" 400 branch instead of a more specific error. Not worth guarding against now given it's an internal protocol, just flagging for awareness.
  3. Cross-language constant duplication: RAW_RENDER_CONTENT_TYPE / RAW_RENDER_HEADER_PREFIX are hand-duplicated in worker.ts, request.rb, and bench-node-renderer.rb. Already called out by an inline comment ("must stay in sync"), so this is a conscious tradeoff rather than an oversight — just noting the drift risk for future changes to the field list.

Security

  • Timing-safe password comparison (authenticate) is unaffected by this change — the raw path funnels into the same precheck function.
  • No user-controlled input flows into the injected header values: protocolVersion/gemVersion/railsEnv come from the gem/config, and password comes from server-side configuration, not client input — so the CRLF-injection guard is solid defense-in-depth rather than closing an actual exploitable path today.
  • The reflected-XSS hardening (text/plain + nosniff on every string response, not just >=400 ones) is a real improvement and is well justified by the inline comment.

Verdict

Solid, carefully-reasoned change with strong test coverage on both the Ruby and Node sides. No correctness or security bugs found in the diff — only the minor style/robustness notes above, none of which block merging.

@github-actions

Copy link
Copy Markdown
Contributor

Pro Node Renderer Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
Pro Node Renderer: simple_eval (non-RSC) 1935.89 ▼6.3% (2066.14) 4.58 ▲3.9% (4.41) 6.39 ▲12.8% (5.66) 200=58081
Pro Node Renderer: react_ssr (non-RSC) 1740.2 ▼5.2% (1835.86) 5.28 ▲5.8% (4.99) 6.39 ▲3.5% (6.17) 200=52208

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

Copy link
Copy Markdown
Contributor

Core Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/: Core 3.35 ▼6.9% (3.6) 2402.97 ▲7.0% (2244.89) 2916.95 ▲3.8% (2809.62) 200=111
/client_side_hello_world: Core 613.22 ▼5.6% (649.29) 9.51 ▼2.1% (9.72) 25.08 ▲22.0% (20.55) 200=18526
/client_side_rescript_hello_world: Core 704.33 ▲8.2% (650.78) 8.5 ▼12.4% (9.7) 15.12 ▼22.4% (19.49) 200=21284
/client_side_hello_world_shared_store: Core 444.37 ▼28.4% (620.99) 12.83 ▲29.4% (9.92) 19.06 ▼4.3% (19.92) 200=13434
/client_side_hello_world_shared_store_controller: Core 620.32 ▲0.3% (618.36) 12.99 ▲30.9% (9.92) 24.3 ▲11.4% (21.82) 200=18633
/client_side_hello_world_shared_store_defer: Core 635.34 ▲3.5% (613.77) 10.29 ▲2.3% (10.06) 25.48 ▲21.1% (21.04) 200=19195
/server_side_hello_world_shared_store: Core 14.76 ▼1.6% (15.0) 570.33 ▲13.5% (502.51) 747.4 ▲10.7% (675.29) 200=452
/server_side_hello_world_shared_store_controller: Core 14.9 ▼2.2% (15.24) 565.14 ▲10.3% (512.32) 733.97 ▲6.9% (686.48) 200=457
/server_side_hello_world_shared_store_defer: Core 14.94 ▼1.2% (15.13) 571.45 ▲16.8% (489.14) 712.28 ▲6.0% (671.7) 200=459
/server_side_hello_world: Core 29.88 ▼0.7% (30.08) 286.47 ▲13.1% (253.21) 326.13 ▲3.0% (316.6) 200=909
/server_side_hello_world_hooks: Core 23.77 ▼21.3% (30.21) 251.37 ▲1.9% (246.72) 402.24 ▲28.8% (312.4) 200=724
/server_side_hello_world_props: Core 29.53 ▼0.4% (29.64) 288.25 ▲10.5% (260.84) 328.78 ▲2.8% (319.7) 200=898
/client_side_log_throw: Core 475.6 ▼27.2% (653.74) 11.88 ▲24.2% (9.56) 17.84 ▼8.7% (19.54) 200=14370
/server_side_log_throw: Core 28.33 ▼5.6% (30.02) 297.4 ▲13.9% (261.03) 341.53 ▲6.1% (321.89) 200=864
/server_side_log_throw_plain_js: Core 28.5 ▼5.0% (30.0) 293.88 ▲18.2% (248.62) 336.41 ▲7.8% (311.97) 200=869
/server_side_log_throw_raise: Core 24.21 ▼19.5% (30.06) 243.11 ▼1.7% (247.3) 279.28 ▼11.1% (314.1) 3xx=741
/server_side_log_throw_raise_invoker: Core 709.94 ▼8.4% (775.22) 9.44 ▲15.4% (8.18) 19.2 ▲19.8% (16.03) 200=21449
/server_side_hello_world_es5: Core 29.62 ▼2.3% (30.33) 287.76 ▲16.6% (246.84) 328.33 ▲4.9% (313.14) 200=900
/server_side_redux_app: Core 28.43 ▼2.7% (29.21) 295.69 ▲18.2% (250.25) 347.08 ▲9.6% (316.7) 200=865
/server_side_hello_world_with_options: Core 29.07 ▼5.2% (30.67) 111.38 ▼55.5% (250.2) 313.44 ▲0.5% (311.91) 200=892
/server_side_redux_app_cached: Core 604.72 ▼9.0% (664.7) 11.03 ▲6.8% (10.33) 20.6 ▲4.0% (19.81) 200=18269
/client_side_manual_render: Core 565.52 ▼13.8% (655.84) 10.23 ▲6.5% (9.6) 24.22 ▲26.3% (19.18) 200=17089
/render_js: Core 22.71 ▼28.8% (31.92) 270.73 ▲17.6% (230.25) 388.15 ▲33.8% (290.2) 200=690
/react_router: Core 27.38 ▼3.6% (28.39) 118.3 ▼55.8% (267.61) 332.31 ▼1.9% (338.87) 200=842
/pure_component: Core 29.52 ▼3.2% (30.49) 287.29 ▲14.4% (251.2) 324.62 ▲3.7% (312.92) 200=900
/react_compiler_example: Core 28.83 ▼6.1% (30.69) 291.98 ▲18.1% (247.31) 332.12 ▲4.4% (318.2) 200=880
/css_modules_images_fonts_example: Core 28.85 ▼4.9% (30.33) 290.93 ▲12.3% (259.07) 332.34 ▲4.8% (317.27) 200=881
/turbolinks_cache_disabled: Core 395.4 ▼39.2% (649.86) 14.54 ▲50.9% (9.63) 20.59 ▲7.6% (19.13) 200=11946
/rendered_html: Core 29.81 ▼1.7% (30.34) 281.8 ▲13.3% (248.72) 329.76 ▲6.1% (310.73) 200=906
/xhr_refresh: Core 11.95 ▼23.3% (15.59) 508.14 ▲3.0% (493.26) 653.75 ▼0.5% (656.97) 200=366
/react_helmet: Core 23.64 ▼21.0% (29.91) 247.01 ▼1.9% (251.74) 296.82 ▼5.1% (312.86) 200=722
/broken_app: Core 28.54 ▼5.1% (30.07) 297.11 ▲14.3% (259.89) 340.86 ▲5.4% (323.27) 200=869
/image_example: Core 29.5 ▼0.6% (29.68) 285.49 ▲11.9% (255.19) 332.13 ▲3.8% (320.11) 200=897
/font_optimization_example: Core 716.76 ▼2.4% (734.62) 8.47 ▼1.7% (8.62) 20.94 ▲10.0% (19.04) 200=21656
/client_side_activity: Core 585.93 ▼10.6% (655.07) 10.01 ▲4.7% (9.56) 22.88 ▲16.4% (19.66) 200=17705
/server_side_activity: Core 28.92 ▼4.2% (30.19) 291.49 ▲19.2% (244.63) 332.61 ▲6.1% (313.55) 200=882
/turbo_frame_tag_hello_world: Core 622.73 ▼16.3% (744.44) 8.12 ▼2.6% (8.34) 17.55 0.0% (17.54) 200=18945
/manual_render_test: Core 596.54 ▼10.3% (665.12) 9.96 ▲4.9% (9.49) 22.6 ▲17.8% (19.19) 200=17908
/root_error_callbacks: Core 29.52 ▼4.5% (30.92) 287.63 ▲22.0% (235.85) 338.03 ▲9.7% (308.26) 200=899
/hydration_scheduling: Core 9.87 ▼7.1% (10.62) 820.1 ▲19.5% (686.18) 1194.78 ▲15.7% (1032.43) 200=311
/rails_form: Core 592.76 ▼11.6% (670.66) 9.8 ▲4.1% (9.41) 25.97 ▲36.1% (19.08) 200=17909
/typed_rails_action: Core 597.15 ▼8.4% (651.97) 9.67 ▼0.3% (9.7) 26.41 ▲37.8% (19.17) 200=18042

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

Copy link
Copy Markdown
Contributor

Pro (shard 1/2) Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/: Pro 75.61 🟢 47.3% (51.32) 83.04 ▼39.9% (138.15) 129.28 ▼33.0% (192.87) 200=2291
/error_scenarios_hub: Pro 463.02 🟢 37.2% (337.49) 16.72 ▼20.5% (21.04) 26.33 ▼20.8% (33.26) 200=13988
/ssr_async_error: Pro 2.9 ▼12.2% (3.3) 2009.69 ▼0.1% (2011.76) 2040.87 ▼9.3% (2250.51) 200=94
/ssr_async_prop_error: Pro 1.4 ▲15.5% (1.21) 5017.98 0.0% (5019.35) 5342.2 ▼38.5% (8692.33) 200=50
/non_existing_react_component: Pro 239.94 🟢 43.8% (166.91) 31.93 ▼24.6% (42.37) 48.7 ▼29.8% (69.42) 200=7252
/non_existing_rsc_payload: Pro 254.78 🟢 46.3% (174.2) 28.95 ▼28.4% (40.44) 48.47 ▼26.2% (65.68) 200=7700
/cached_react_helmet: Pro 478.41 🟢 40.3% (340.98) 11.43 ▼45.4% (20.95) 22.25 ▼37.1% (35.35) 200=14554
/cached_redux_component: Pro 499.97 🟢 40.7% (355.45) 15.83 ▼23.3% (20.64) 24.97 ▼22.4% (32.2) 200=15104
/lazy_apollo_graphql: Pro 206.06 🟢 50.0% (137.33) 31.41 ▼40.3% (52.6) 50.44 ▼41.4% (86.05) 200=6231
/redis_receiver: Pro 131.18 🟢 42.4% (92.15) 58.47 ▼18.0% (71.32) 124.5 ▼16.6% (149.26) 200=3946
/stream_shell_error_demo: Pro 152.53 ▲12.5% (135.58) 28.2 ▼36.8% (44.59) 39.47 ▼53.0% (84.06) 200=5792
/test_incremental_rendering: Pro 4.2 ▲29.6% (3.24) 2008.61 0.0% (2009.24) 2019.6 ▼14.9% (2374.04) 200=136
/rsc_posts_page_over_redis: Pro 145.74 🟢 58.5% (91.98) 51.0 ▼30.9% (73.83) 76.15 ▼38.0% (122.74) 200=4408
/rsc_fouc_probe: Pro 225.83 🟢 57.6% (143.29) 14.82 🟢 67.1% (44.99) 41.84 ▼49.2% (82.3) 200=6878
/async_on_server_sync_on_client: Pro 2.19 ▼0.3% (2.2) 3009.25 0.0% (3010.69) 3031.21 ▼16.1% (3614.28) 200=73
/server_router: Pro 232.11 🟢 55.2% (149.53) 14.29 🟢 67.9% (44.55) 42.73 ▼44.4% (76.84) 200=7064
/unwrapped_rsc_route_client_render: Pro 455.83 ▲33.2% (342.17) 5.95 ▼69.7% (19.63) 14.58 ▼53.4% (31.27) 200=13866
/async_render_function_returns_string: Pro 249.96 🟢 56.5% (159.76) 34.06 ▼14.2% (39.7) 47.29 ▼32.8% (70.4) 200=7553
/async_components_demo: Pro 6.7 ▼1.3% (6.79) 1021.9 ▼0.4% (1026.1) 1051.58 ▼1.4% (1066.4) 200=213
/stream_native_metadata: Pro 242.55 🟢 49.1% (162.72) 30.25 ▼30.7% (43.66) 49.88 ▼30.9% (72.18) 200=7331
/rsc_native_metadata: Pro 5.86 ▼12.5% (6.7) 1009.77 ▼0.1% (1010.76) 1029.74 ▼1.9% (1050.16) 200=184
/react_intl_rsc_demo: Pro 108.17 🟢 34.7% (80.28) 64.55 ▼18.6% (79.32) 107.26 ▼20.0% (134.0) 200=3272
/client_side_hello_world_shared_store: Pro 467.39 🟢 44.1% (324.41) 12.29 ▼44.9% (22.3) 22.53 ▼37.2% (35.85) 200=14126
/client_side_hello_world_shared_store_defer: Pro 468.06 🟢 49.9% (312.31) 18.87 ▼15.5% (22.33) 25.18 ▼30.2% (36.07) 200=14140
/server_side_hello_world_shared_store_controller: Pro 177.56 🟢 60.8% (110.42) 43.23 ▼29.5% (61.36) 61.75 ▼39.0% (101.17) 200=5369
/server_side_hello_world: Pro 243.39 🟢 47.8% (164.65) 25.41 ▼38.2% (41.1) 42.33 ▼37.3% (67.5) 200=7357
/client_side_log_throw: Pro 420.88 ▲23.1% (341.99) 13.86 ▼33.2% (20.74) 23.13 ▼31.2% (33.61) 200=12716
/server_side_log_throw_plain_js: Pro 509.63 🟢 43.3% (355.66) 15.79 ▼23.1% (20.54) 24.32 ▼23.4% (31.75) 200=15399
/server_side_log_throw_raise: Pro 324.19 ▲49.8% (216.45) 22.94 ▼22.0% (29.43) 35.52 ▼29.1% (50.12) 3xx=9793
/server_side_hello_world_es5: Pro 250.04 🟢 48.4% (168.49) 33.96 ▼18.4% (41.6) 46.72 ▼33.6% (70.33) 200=7557
/server_side_hello_world_with_options: Pro 216.93 ▲26.1% (172.08) 26.95 ▼34.4% (41.06) 44.25 ▼34.0% (67.03) 200=6558
/client_side_manual_render: Pro 494.74 🟢 44.3% (342.81) 15.7 ▼23.7% (20.58) 23.17 ▼28.9% (32.59) 200=14949
/react_router: Pro 311.49 🟢 68.3% (185.06) 23.82 ▼35.6% (36.98) 38.16 ▼39.0% (62.59) 200=9414
/css_modules_images_fonts_example: Pro 210.84 ▲24.6% (169.22) 27.47 ▼33.3% (41.18) 58.11 ▼12.7% (66.58) 200=6373
/rendered_html: Pro 263.17 🟢 50.8% (174.49) 20.53 ▼48.7% (40.03) 38.64 ▼42.8% (67.52) 200=8011
/react_helmet: Pro 96.54 ▼12.6% (110.4) 77.24 ▲17.6% (65.69) 93.06 ▼14.2% (108.41) 200=2927
/image_example: Pro 211.87 ▲35.7% (156.09) 27.19 ▼32.1% (40.05) 61.55 ▼16.8% (73.97) 200=6406
/posts_page: Pro 197.52 🟢 55.7% (126.87) 37.93 ▼32.4% (56.12) 54.67 ▼39.9% (90.99) 200=5971

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@github-actions

Copy link
Copy Markdown
Contributor

Pro (shard 2/2) Benchmark Summary

Benchmark RPS p50(ms) p90(ms) Status
/empty: Pro 1246.84 ▲1.9% (1223.53) 6.44 ▲8.8% (5.92) 9.55 ▲4.6% (9.13) 200=37660
/ssr_shell_error: Pro 133.38 ▼12.3% (152.15) 44.25 ▼3.7% (45.94) 79.91 ▲8.0% (73.97) 200=4035
/ssr_sync_error: Pro 153.94 ▼2.0% (157.04) 47.32 ▲6.4% (44.49) 70.7 ▼0.5% (71.07) 200=4655
/rsc_component_error: Pro 119.98 ▼16.2% (143.14) 44.19 ▼0.5% (44.4) 66.46 ▼14.0% (77.32) 200=4394,3xx=17
/non_existing_stream_react_component: Pro 153.9 ▼11.2% (173.36) 37.55 ▼7.2% (40.48) 49.81 ▼23.3% (64.9) 200=4684
/server_side_redux_app_cached: Pro 363.35 ▲0.2% (362.58) 21.79 ▲8.0% (20.18) 34.42 ▲7.3% (32.09) 200=10979
/loadable: Pro 126.95 ▼12.1% (144.43) 45.16 ▼1.4% (45.81) 64.7 ▼16.8% (77.77) 200=3864
/apollo_graphql: Pro 116.19 ▼9.7% (128.6) 39.56 ▼21.0% (50.06) 62.37 ▼23.3% (81.35) 200=3539
/console_logs_in_async_server: Pro 3.56 ▲10.0% (3.24) 2124.33 0.0% (2125.26) 2144.38 ▼1.0% (2166.12) 200=122
/stream_error_demo: Pro 3.32 ▼1.3% (3.36) 2012.43 0.0% (2011.58) 2032.05 ▼10.4% (2268.22) 200=108
/stream_async_components: Pro 124.84 ▼2.4% (127.94) 46.66 ▲2.7% (45.44) 74.71 ▼7.0% (80.36) 200=4403,3xx=52
/rsc_posts_page_over_http: Pro 126.23 ▼7.0% (135.69) 46.89 ▼7.4% (50.61) 93.28 ▲9.1% (85.49) 200=3819
/rsc_echo_props: Pro 59.47 ▼0.4% (59.73) 99.59 ▼13.5% (115.11) 167.05 ▼4.9% (175.58) 200=1802
/client_side_fouc_probe: Pro 293.46 ▼17.3% (354.95) 19.34 ▼3.2% (19.97) 31.12 ▼5.0% (32.75) 200=8869
/async_on_server_sync_on_client_client_render: Pro 339.15 ▲3.6% (327.48) 21.83 ▲7.1% (20.38) 35.26 ▼1.8% (35.89) 200=10250
/server_router_client_render: Pro 330.6 ▼2.8% (339.98) 22.99 ▲6.9% (21.5) 37.64 ▲11.8% (33.68) 200=9991
/unwrapped_rsc_route_stream_render: Pro 175.02 ▲4.4% (167.57) 42.58 ▲5.1% (40.53) 69.32 ▼1.1% (70.09) 200=5290
/async_render_function_returns_component: Pro 181.6 ▲1.3% (179.35) 34.96 ▼7.8% (37.91) 57.89 ▼10.6% (64.75) 200=5492
/native_metadata: Pro 170.47 ▲1.1% (168.68) 43.28 ▲12.4% (38.51) 65.82 ▲2.4% (64.29) 200=5106,3xx=16
/hybrid_metadata_streaming: Pro 176.39 ▲4.4% (168.93) 44.07 ▲7.1% (41.16) 69.2 ▼0.8% (69.76) 200=5330
/cache_demo: Pro 117.23 ▼5.3% (123.82) 55.54 ▲2.8% (54.01) 81.61 ▼7.2% (87.93) 200=3549
/client_side_hello_world: Pro 292.42 ▼13.7% (338.93) 19.35 ▼2.7% (19.9) 23.55 ▼28.4% (32.88) 200=8895
/client_side_hello_world_shared_store_controller: Pro 267.12 ▼16.8% (321.05) 21.55 ▼0.1% (21.57) 52.55 ▲45.5% (36.11) 200=8073
/server_side_hello_world_shared_store: Pro 128.71 ▲6.2% (121.19) 42.82 ▼30.0% (61.15) 76.37 ▼19.8% (95.27) 200=3921
/server_side_hello_world_shared_store_defer: Pro 112.12 ▼8.3% (122.24) 52.31 ▼8.5% (57.15) 128.38 ▲40.7% (91.27) 200=3392
/server_side_hello_world_hooks: Pro 187.09 ▲5.5% (177.32) 40.73 ▲10.4% (36.89) 64.28 ▲0.3% (64.08) 200=5643,3xx=16
/server_side_log_throw: Pro 170.1 ▲3.2% (164.9) 44.13 ▲7.6% (41.02) 70.43 ▲1.0% (69.73) 200=5143
/source_mapped_prerender_error_probe: Pro 253.97 ▲3.5% (245.44) 29.2 ▲6.3% (27.48) 44.14 ▼4.4% (46.15) 3xx=7676
/server_side_log_throw_raise_invoker: Pro 400.58 ▼1.4% (406.26) 15.64 ▼11.2% (17.61) 26.3 ▼9.0% (28.89) 200=12106
/server_side_redux_app: Pro 158.87 ▲1.3% (156.83) 53.06 ▲25.7% (42.22) 76.55 ▲4.1% (73.53) 200=4788,3xx=13
/server_side_redux_app_cached: Pro 358.98 ▼1.0% (362.58) 21.83 ▲8.2% (20.18) 31.82 ▼0.8% (32.09) 200=10848
/render_js: Pro 310.21 ▼16.3% (370.75) 18.3 ▼6.5% (19.57) 21.8 ▼31.5% (31.83) 200=9436
/pure_component: Pro 153.56 ▼11.6% (173.79) 37.47 ▼5.7% (39.75) 53.06 ▼22.4% (68.41) 200=4646
/turbolinks_cache_disabled: Pro 341.15 ▼2.4% (349.58) 16.07 ▼18.7% (19.78) 30.62 ▼2.9% (31.55) 200=10378
/xhr_refresh: Pro 116.85 ▼7.0% (125.62) 49.47 ▼10.2% (55.06) 65.73 ▼33.6% (99.03) 200=3536
/broken_app: Pro 174.44 ▼1.6% (177.29) 42.49 ▲5.1% (40.44) 68.21 ▲2.7% (66.39) 200=5274
/server_render_with_timeout: Pro 58.78 ▼1.1% (59.42) 117.07 ▲0.9% (115.98) 127.64 ▼5.0% (134.38) 200=1788

▲/▼ non-zero change vs baseline · 0.0% exact/near-zero match · 🔴 significant regression · 🟢 significant improvement (tracked measures) · (n) = baseline

@justin808

Copy link
Copy Markdown
Member

PR-Batch Handoff — continuation lane ror4579-lane1-koa

Final state: ready-no-merge-authority · merge_authority: none (maintainer merges)

Lane Card — holder: fable-m5-pr4579-cont (claim released) · branch: alexeyr/3584/raw-rendering-request · head: ab0b98c6a · phase: final · pr_url: #4579 · dashboard_url: UNKNOWN

Immediate maintainer attention

  • Merge decision is yours. All current-head gates are green and all review threads resolved. The PR is still draft (left as the author set it) — mark ready and merge when satisfied. The merge ledger's complete_allowed: false comes only from the draft flag plus a changelog-classification UNKNOWN (the [Unreleased] entry is in this PR's diff; the classifier evaluates merged state).

FYI / decisions made

  • Gates on head ab0b98c6a: 49 pass / 5 skip / 0 fail — required-pr-gate ✓, CodeQL ✓ (0 open alerts; the earlier high-severity js/reflected-xss is cleared), full hosted CI fleet ✓ (requested via +ci-run-hosted; one stale gate race on b62f91be5 was rerun), Core + Pro + Node-Renderer benchmark suites ✓. CodeRabbit APPROVED.
  • Six review threads addressed and resolved across three commits:
    • b62f91be5 — auth-first prechecks on the raw path; uniform CRLF sanitization of all raw metadata headers (+spec); @fastify/formbody restored for legacy-gem rolling-deploy compat (+urlencoded round-trip test); setResponse hardened to explicit text/plain + nosniff for every non-stream string payload (CodeQL fix); BUNDLED WITH reverted to 2.5.4; CHANGELOG [Pro] entry added.
    • 3b7c5f18b — forwarded gemVersion/railsEnv to raw prechecks (review caught a real regression: the dev-mode version-mismatch 412 was being skipped; +test).
    • ab0b98c6a — precheck body omits absent headers so the 412 "received fields" diagnostic stays truthful (+test); ResponseResult types Content-Type as never so future callers can't be silently downgraded.
  • Decision log (3 non-blocking decisions) is in the PR description: formbody kept this release (drop with next PROTOCOL_VERSION bump); unconditional text/plain with type-level lock; lockfile hygiene revert.
  • Validation: worker jest 62/62; Pro rspec 104 examples / 0 failures; rubocop, RBS, tsc, eslint, prettier all clean. Local streaming jest suites fail on a clean checkout too (environmental); hosted CI is authoritative and green.
  • QA lane: not required — internal wire-protocol change fully exercised by unit/integration suites, hosted CI, and the PR's benchmark runs; no UI surface. (No qa-evidence replay marker for that reason.)
  • Lockfile evidence: net package.json + pnpm-lock.yaml diff vs main is zero after the formbody restore (ledger lockfile_diff: false); react_on_rails_pro/Gemfile.lock matches main.
  • Confidence note: high on the Ruby/Node handshake changes (direct tests on both sides); the only UNKNOWN is environmental local streaming-suite flakiness, which hosted CI supersedes.

🤖 Generated with Claude Code

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Evaluate outer wire encoding for renderer requests (urlencoded → raw / JSON / MessagePack)

3 participants