Fix Content-Length mismatch and null renderingRequest errors in node renderer#3069
Fix Content-Length mismatch and null renderingRequest errors in node renderer#3069
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a configurable renderer HTTP keep-alive timeout (default 30s), wires it into the HTTP client, validates and tests the config, and adjusts node-renderer invalid-request error messaging and its test expectation (adds Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Rails as Rails App
participant HTTPX as HTTPX Pool
participant Node as Node Renderer
Rails->>HTTPX: open/pool request (keep_alive_timeout configured)
HTTPX->>Node: send HTTP/2 request (body + Content-Length)
Node->>Node: validate renderingRequest (null/array/empty-string)
alt invalid renderingRequest
Node->>Rails: respond 400 with detailed diagnostic (type, filtered body keys)
else valid renderingRequest
Node->>Node: render and return result
Node->>Rails: respond 200 with render result
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
ReviewGood diagnostic improvement overall. The two-layer fix (upstream validation in the route handler + better IssuesMissing validation for Missing test for empty-string Body keys forwarded to error reporters (see inline) — Observations
|
size-limit report 📦
|
Greptile SummaryThis PR fixes two related production errors: stale HTTPX persistent connections causing Confidence Score: 5/5Safe to merge — all remaining findings are non-blocking P2 style suggestions The core logic is sound: the keep_alive_timeout fix directly addresses the root cause, and the early renderingRequest validation with a type guard correctly narrows the type for downstream use. Both new tests are meaningful regression coverage. No P0 or P1 issues found. No files require special attention before merge Important Files Changed
Sequence DiagramsequenceDiagram
participant Rails as Rails (HTTPX client)
participant Node as Node Renderer (Fastify)
Note over Rails,Node: keep_alive_timeout=30s closes idle connections before they go stale
Rails->>Node: POST /bundles/:ts/render/:digest (multipart)
alt Body framing valid
Node->>Node: isValidRenderingRequest(renderingRequest)
alt renderingRequest is non-empty string
Node->>Node: handleRenderRequest()
Node-->>Rails: 200 OK + SSR result
else renderingRequest missing / null / empty
Node-->>Rails: 400 Bad Request + actionable message
end
else FST_ERR_CTP_INVALID_CONTENT_LENGTH
Node->>Node: onError hook → log "Invalid request body framing"
Node-->>Rails: 400 Content-Length mismatch
Note over Rails: Connection closed; next request opens fresh stream
end
Reviews (1): Last reviewed commit: "Add keep_alive_timeout to HTTPX renderer..." | Re-trigger Greptile |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/react-on-rails-pro-node-renderer/tests/worker.test.ts (1)
104-145: Add one test for empty-stringrenderingRequestto fully cover the new validator.You now validate non-empty strings, but only
missingandnullare asserted here.Proposed test addition
+ test('POST /bundles/:bundleTimestamp/render/:renderRequestDigest returns actionable error when renderingRequest is empty string', async () => { + const app = worker({ + serverBundleCachePath: serverBundleCachePathForTest(), + }); + + const res = await app + .inject() + .post(`/bundles/${BUNDLE_TIMESTAMP}/render/d41d8cd98f00b204e9800998ecf8427e`) + .payload({ + gemVersion, + protocolVersion, + railsEnv, + renderingRequest: '', + }) + .end(); + + expect(res.statusCode).toBe(400); + expect(res.payload).toContain('Invalid "renderingRequest" field in render request.'); + expect(res.payload).toContain('Received type: string.'); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/react-on-rails-pro-node-renderer/tests/worker.test.ts` around lines 104 - 145, Add a third test alongside the two existing cases in worker.test.ts that posts to /bundles/:bundleTimestamp/render/:renderRequestDigest with renderingRequest set to an empty string (""), using the same app setup (worker(...) with serverBundleCachePathForTest()) and payload fields (gemVersion, protocolVersion, railsEnv); assert the response status is 400 and that res.payload contains the same invalid field message ('Invalid "renderingRequest" field in render request.'), the specific received-type message for an empty string (e.g. 'Received type: empty string.'), and the actionable hint ('Likely causes: request body truncation') so the validator is fully covered for missing, null, and empty-string cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/react-on-rails-pro-node-renderer/tests/worker.test.ts`:
- Around line 104-145: Add a third test alongside the two existing cases in
worker.test.ts that posts to
/bundles/:bundleTimestamp/render/:renderRequestDigest with renderingRequest set
to an empty string (""), using the same app setup (worker(...) with
serverBundleCachePathForTest()) and payload fields (gemVersion, protocolVersion,
railsEnv); assert the response status is 400 and that res.payload contains the
same invalid field message ('Invalid "renderingRequest" field in render
request.'), the specific received-type message for an empty string (e.g.
'Received type: empty string.'), and the actionable hint ('Likely causes:
request body truncation') so the validator is fully covered for missing, null,
and empty-string cases.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7a27081d-dd61-4943-af1a-bb2f284273ef
📒 Files selected for processing (5)
packages/react-on-rails-pro-node-renderer/src/worker.tspackages/react-on-rails-pro-node-renderer/src/worker/handleRenderRequest.tspackages/react-on-rails-pro-node-renderer/tests/worker.test.tsreact_on_rails_pro/lib/react_on_rails_pro/configuration.rbreact_on_rails_pro/lib/react_on_rails_pro/request.rb
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2fdaaaf572
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Review SummaryOverall the approach is correct and addresses a real production issue. The error chain (Content-Length mismatch → body parsing failure → null renderingRequest) is well-diagnosed, and the early validation with descriptive error messages is a meaningful improvement. A few things to address before merging: Security — incomplete sensitive-key filtering (inline comment)
Potential noise — double reporting on Content-Length mismatch (inline comment)When Missing spec coverage for keep_alive_timeout (inline comment)The new Minor — test count in PR descriptionThe description says "2 new tests" but the diff adds 4 tests (missing, null, empty string, and password-key filtering). Not a code issue, just a mismatch. |
|
Opened #3071 to track the production errors this PR addresses. We couldn't reproduce locally, but the fix targets the right root cause. Merging and will monitor. |
AbanoubGhadban
left a comment
There was a problem hiding this comment.
LGTM. This addresses the production errors tracked in #3071. The early validation, Fastify error hook, and HTTPX keep_alive_timeout all look correct. Merging to verify the fix in production.
Review feedback addressed — all threads resolvedCode changes (3 commits on
|
| # | File | Comment | Status |
|---|---|---|---|
| 1 | request.rb:303 |
stream_spec keep_alive_timeout sync | Fixed in 54b3cd0 |
| 2 | worker.ts:180 |
Body keys in error reporters | Acknowledged; 14 sensitive keys now filtered |
| 3 | worker.ts:151 |
Empty-string test missing | Fixed in a05ba36 |
| 4 | worker.ts:337 |
Body type widened | Informational; type predicate narrows correctly |
| 5 | configuration.rb:129 |
keep_alive_timeout validation | Fixed in a05ba36 |
| 6 | worker.ts:164-183 |
Empty-string type label | Fixed in a05ba36 |
| 7 | configuration.rb:48 |
keep_alive_timeout validation (dup) | Fixed in a05ba36 |
| 8 | request.rb:303 |
Test doubles breakage claim | Verified not an issue |
| 9 | worker.ts |
Sensitive key set too narrow | Fixed in a05ba36 |
| 10 | worker.ts:359 |
Double error reporting | Already handled by badRequestResponseResult |
All 10 review threads replied to and resolved.
Prevents Content-Length mismatch errors (FST_ERR_CTP_INVALID_CONTENT_LENGTH) caused by writing into stale HTTP/2 persistent connections. HTTPX will now proactively close connections idle for 30 seconds, allowing the retry plugin to open a fresh connection on the next request. Configurable via `renderer_http_keep_alive_timeout` (default: 30s). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Expand SENSITIVE_REQUEST_BODY_KEYS to include token, secret, api_key, auth_token, authorization, credentials (not just password) - Label empty-string renderingRequest as 'empty string' instead of misleading 'string' in diagnostic messages - Add validation setter for renderer_http_keep_alive_timeout (must be a positive number or nil), matching concurrent_component_streaming_buffer_size - Add configuration specs for renderer_http_keep_alive_timeout validation - Update worker tests for new sensitive key filtering and empty-string label Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
af03bd1 to
7183faa
Compare
Review: Fix Content-Length mismatch and null renderingRequest errorsOverall this is a solid, well-reasoned fix with good test coverage. The root-cause analysis is accurate — stale HTTP/2 connections causing truncated bodies is a real and non-obvious failure mode. The three-layer approach (prevent staleness, validate early, improve diagnostics) is the right pattern for production hardening. A few things to flag: Minor logic imprecision in
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is ON, but it could not run because on-demand usage is turned off. To enable Bugbot Autofix, turn on on-demand usage and set a spend limit in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 7183faa. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7183faa7a9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
🧹 Nitpick comments (2)
react_on_rails_pro/lib/react_on_rails_pro/request.rb (1)
307-313: Include keep-alive timeout in connection error diagnostics.When connection creation fails, logging the new keep-alive setting would make misconfiguration triage faster.
Suggested diff
message = <<~MSG [ReactOnRailsPro] Error creating HTTPX connection. renderer_http_pool_size = #{ReactOnRailsPro.configuration.renderer_http_pool_size} renderer_http_pool_timeout = #{ReactOnRailsPro.configuration.renderer_http_pool_timeout} renderer_http_pool_warn_timeout = #{ReactOnRailsPro.configuration.renderer_http_pool_warn_timeout} + renderer_http_keep_alive_timeout = #{ReactOnRailsPro.configuration.renderer_http_keep_alive_timeout} renderer_url = #{url} Be sure to use a url that contains the protocol of http or https. Original error is #{e} MSG🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@react_on_rails_pro/lib/react_on_rails_pro/request.rb` around lines 307 - 313, The connection error diagnostic message built in Request (where `message` is constructed) is missing the keep-alive timeout; update the error message string to include the keep-alive setting (e.g. `ReactOnRailsPro.configuration.renderer_http_keepalive_timeout` or the actual keep-alive config name used in your code) so the log prints the keep-alive timeout alongside `renderer_http_pool_size`, `renderer_http_pool_timeout`, `renderer_http_pool_warn_timeout`, and `renderer_url`; modify the `message` construction in `ReactOnRailsPro::Request` (the same block that sets `message = <<~MSG`) to interpolate the keep-alive config value for easier triage.react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb (1)
433-437: Prefer constant-based default expectation.Using the constant here avoids brittle duplication if the default changes later.
Suggested diff
- it "defaults to 30" do + it "defaults to the configured constant" do ReactOnRailsPro.configure {} # rubocop:disable Lint/EmptyBlock - expect(ReactOnRailsPro.configuration.renderer_http_keep_alive_timeout).to eq(30) + expect(ReactOnRailsPro.configuration.renderer_http_keep_alive_timeout) + .to eq(ReactOnRailsPro::Configuration::DEFAULT_RENDERER_HTTP_KEEP_ALIVE_TIMEOUT) end🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb` around lines 433 - 437, Replace the hardcoded 30 with the configuration default constant: update the spec to expect ReactOnRailsPro.configuration.renderer_http_keep_alive_timeout to equal the constant that defines the default timeout (e.g., ReactOnRailsPro::Configuration::DEFAULT_RENDERER_HTTP_KEEP_ALIVE_TIMEOUT or the actual constant name found in the configuration class/module) instead of 30 so the test follows the source-of-truth constant used by ReactOnRailsPro.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@react_on_rails_pro/lib/react_on_rails_pro/request.rb`:
- Around line 307-313: The connection error diagnostic message built in Request
(where `message` is constructed) is missing the keep-alive timeout; update the
error message string to include the keep-alive setting (e.g.
`ReactOnRailsPro.configuration.renderer_http_keepalive_timeout` or the actual
keep-alive config name used in your code) so the log prints the keep-alive
timeout alongside `renderer_http_pool_size`, `renderer_http_pool_timeout`,
`renderer_http_pool_warn_timeout`, and `renderer_url`; modify the `message`
construction in `ReactOnRailsPro::Request` (the same block that sets `message =
<<~MSG`) to interpolate the keep-alive config value for easier triage.
In `@react_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb`:
- Around line 433-437: Replace the hardcoded 30 with the configuration default
constant: update the spec to expect
ReactOnRailsPro.configuration.renderer_http_keep_alive_timeout to equal the
constant that defines the default timeout (e.g.,
ReactOnRailsPro::Configuration::DEFAULT_RENDERER_HTTP_KEEP_ALIVE_TIMEOUT or the
actual constant name found in the configuration class/module) instead of 30 so
the test follows the source-of-truth constant used by ReactOnRailsPro.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 49f32f83-9383-494c-aea8-a644c3adaa12
📒 Files selected for processing (5)
packages/react-on-rails-pro-node-renderer/src/worker.tspackages/react-on-rails-pro-node-renderer/tests/worker.test.tsreact_on_rails_pro/lib/react_on_rails_pro/configuration.rbreact_on_rails_pro/lib/react_on_rails_pro/request.rbreact_on_rails_pro/spec/react_on_rails_pro/configuration_spec.rb
✅ Files skipped from review due to trivial changes (1)
- packages/react-on-rails-pro-node-renderer/tests/worker.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/react-on-rails-pro-node-renderer/src/worker.ts
ReviewThe root-cause analysis and layered defense strategy are well thought out — stale connection prevention at the transport layer plus early validation with actionable diagnostics is the right approach. Tests are thorough and cover the meaningful edge cases. A few notes: Correctness: The initial Simplification: The Security: Tests: The validator accepts any Question on the 30-second default: The goal is to close idle connections before the node renderer closes them first. Does the node renderer have an explicit HTTP/2 idle timeout configured? If Fastify/Node has no idle timeout (or a very long one), any client-side value works fine. But if the renderer closes connections after, say, 25 seconds, the 30-second Ruby default would still leave a window where stale connections get picked up. Worth documenting the intended relationship between the two values, even just in a code comment near the constant. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Third round of review feedback addressed — all threads resolvedCode change (commit a298f04)
Declined with rationale (4 threads)
Already addressed (2 duplicate threads)
All 6 unresolved threads now resolved. Zero open review threads remaining. |
|
Overall this is a clean, well-scoped fix. The root cause analysis is accurate and the layered approach (prevent stale connections + early validation + actionable diagnostics) is solid. A few notes below. Issue 1: The setter guard Issue 2: Client timeout should be shorter than the server-side idle timeout The default of 30 s is only correct if the node renderer Fastify server keeps connections open for longer than 30 s. If Fastify own keep-alive/idle timeout is <= 30 s, writes can still land on a just-closed connection in the race window. Worth documenting the relationship between this setting and Fastify keepAliveTimeout so operators know to set the client value shorter than the server value. Nitpick: No CHANGELOG entry The new Nitpick: Hard-coded default in upload_asset_spec.rb The fixture hard-codes |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fourth round of review feedback addressed — all threads resolvedCode change (commit 818020c)
Declined with rationale (2 threads)
All 3 threads resolved. Zero open review threads remaining. |
|
TEST COMMENT - PLEASE IGNORE - will be deleted |
|
TEST COMMENT REPLACED |
Review: Fix Content-Length mismatch and null renderingRequest errorsOverall this is a well-structured fix with good defense-in-depth layering (root cause fix + early validation + improved diagnostics). The test coverage is thorough for the new configuration option and the |
Review: Fix Content-Length mismatch and null renderingRequest errorsOverall this is a well-structured fix with good defense-in-depth layering (root cause fix + early validation + improved diagnostics). The test coverage is thorough for the new configuration option and the Bug:
|
…e-dedup * origin/main: (51 commits) Fix Content-Length mismatch and null renderingRequest errors in node renderer (#3069) Improve memory debugging docs with simpler heap snapshot approach (#3072) Enforce strict version matching in doctor and recommend doctor on errors (#3070) Remove immediate_hydration feature from codebase (#2834) Fix infinite fork loop when node renderer worker fails to bind port (#2881) Fix TanStack Router SSR hydration mismatch in Pro async path (#2932) Improve node renderer error messages for malformed render requests (#3068) Add interactive mode prompt to create-react-on-rails-app (#3063) docs: replace hardcoded version numbers with unversioned install commands (#2893) Remove Contributing section from docs sidebar (#3064) Consolidate docs comparison pages into single evaluation entry (#3065) Docs route cleanup: canonicalize worst verbose URL slugs (#3067) Add agent summary blocks to high-value docs pages (#3066) Fix brittle positional assertions in create-app tests (#2923) Auto-resolve renderer password from ENV in Rails Pro (#2921) Standardize bundle env vars from =yes to =true (#2925) Fix Pro generator multiline and template-literal rewrites (#2918) Fix spec/dummy Procfile.dev SERVER_BUNDLE_ONLY to match template convention (#2922) Add llms docs entry points (#2916) Bump version to 16.6.0.rc.0 ... # Conflicts: # packages/create-react-on-rails-app/src/create-app.ts
…lacement-2835-main * origin/main: (44 commits) Fix Content-Length mismatch and null renderingRequest errors in node renderer (#3069) Improve memory debugging docs with simpler heap snapshot approach (#3072) Enforce strict version matching in doctor and recommend doctor on errors (#3070) Remove immediate_hydration feature from codebase (#2834) Fix infinite fork loop when node renderer worker fails to bind port (#2881) Fix TanStack Router SSR hydration mismatch in Pro async path (#2932) Improve node renderer error messages for malformed render requests (#3068) Add interactive mode prompt to create-react-on-rails-app (#3063) docs: replace hardcoded version numbers with unversioned install commands (#2893) Remove Contributing section from docs sidebar (#3064) Consolidate docs comparison pages into single evaluation entry (#3065) Docs route cleanup: canonicalize worst verbose URL slugs (#3067) Add agent summary blocks to high-value docs pages (#3066) Fix brittle positional assertions in create-app tests (#2923) Auto-resolve renderer password from ENV in Rails Pro (#2921) Standardize bundle env vars from =yes to =true (#2925) Fix Pro generator multiline and template-literal rewrites (#2918) Fix spec/dummy Procfile.dev SERVER_BUNDLE_ONLY to match template convention (#2922) Add llms docs entry points (#2916) Bump version to 16.6.0.rc.0 ... # Conflicts: # CHANGELOG.md
### Summary Adds missing changelog entries to the `[Unreleased]` section for PRs merged since `v16.6.0.rc.0`. Analyzed 19 commits on `origin/main` since the last tag, identified 1 missing user-visible PR (#3069), and skipped 10 non-user-visible PRs (docs, tests, internal refactors). **New entries added:** - **Added**: `[Pro] Configurable HTTP keep-alive timeout for node renderer connections` (PR #3069) - **Fixed**: `[Pro] Fixed SSR failures from stale persistent HTTP/2 connections` (PR #3069) **Entries already present** (added in prior changelog updates): PRs #2834, #2881, #2918, #2921, #2923, #2932, #3063, #3068, #3070. **Skipped** (not user-visible): #2893 (docs), #2916 (docs), #2922 (test fix), #2923 (test fix), #2925 (internal refactor), #3064 (docs), #3065 (docs), #3066 (docs), #3067 (docs), #3072 (docs). ### Pull Request checklist - [x] ~Add/update test to cover these changes~ - [x] ~Update documentation~ - [x] Update CHANGELOG file ### Other Information No code changes — CHANGELOG.md only. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk because this PR only changes documentation (`CHANGELOG.md`) and does not modify runtime code. > > **Overview** > Updates `CHANGELOG.md` *[Unreleased]* to include missing Pro release notes for PR `#3069`, documenting the new `renderer_http_keep_alive_timeout` config and the associated fix for SSR failures from stale persistent HTTP/2 connections to the node renderer. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8d1a480. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Interactive mode selection prompt for CLI tool when no explicit mode is specified * Configurable keep-alive timeout setting for Pro users (default 30 seconds) * **Bug Fixes** * Enhanced validation and error handling for invalid request payloads * Improved template literal handling in code generation * Better HTTP connection stability with enhanced diagnostic messaging <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `credentials` to sensitive-key filtering test coverage - Exclude `renderingRequest` from diagnostic bodyKeys output (already reported via the "Received type:" line) - Document client-side vs server-side keep-alive timeout relationship Fixes #3075 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds missing changelog entries to the `[Unreleased]` section for PRs merged since `v16.6.0.rc.0`. Analyzed 19 commits on `origin/main` since the last tag, identified 1 missing user-visible PR (#3069), and skipped 10 non-user-visible PRs (docs, tests, internal refactors). **New entries added:** - **Added**: `[Pro] Configurable HTTP keep-alive timeout for node renderer connections` (PR #3069) - **Fixed**: `[Pro] Fixed SSR failures from stale persistent HTTP/2 connections` (PR #3069) **Entries already present** (added in prior changelog updates): PRs **Skipped** (not user-visible): #2893 (docs), #2916 (docs), #2922 (test fix), #2923 (test fix), #2925 (internal refactor), #3064 (docs), #3065 (docs), #3066 (docs), #3067 (docs), #3072 (docs). - [x] ~Add/update test to cover these changes~ - [x] ~Update documentation~ - [x] Update CHANGELOG file No code changes — CHANGELOG.md only. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk because this PR only changes documentation (`CHANGELOG.md`) and does not modify runtime code. > > **Overview** > Updates `CHANGELOG.md` *[Unreleased]* to include missing Pro release notes for PR `#3069`, documenting the new `renderer_http_keep_alive_timeout` config and the associated fix for SSR failures from stale persistent HTTP/2 connections to the node renderer. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8d1a480. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> * **New Features** * Interactive mode selection prompt for CLI tool when no explicit mode is specified * Configurable keep-alive timeout setting for Pro users (default 30 seconds) * **Bug Fixes** * Enhanced validation and error handling for invalid request payloads * Improved template literal handling in code generation * Better HTTP connection stability with enhanced diagnostic messaging <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds missing changelog entries to the `[Unreleased]` section for PRs merged since `v16.6.0.rc.0`. Analyzed 19 commits on `origin/main` since the last tag, identified 1 missing user-visible PR (#3069), and skipped 10 non-user-visible PRs (docs, tests, internal refactors). **New entries added:** - **Added**: `[Pro] Configurable HTTP keep-alive timeout for node renderer connections` (PR #3069) - **Fixed**: `[Pro] Fixed SSR failures from stale persistent HTTP/2 connections` (PR #3069) **Entries already present** (added in prior changelog updates): PRs **Skipped** (not user-visible): #2893 (docs), #2916 (docs), #2922 (test fix), #2923 (test fix), #2925 (internal refactor), #3064 (docs), #3065 (docs), #3066 (docs), #3067 (docs), #3072 (docs). - [x] ~Add/update test to cover these changes~ - [x] ~Update documentation~ - [x] Update CHANGELOG file No code changes — CHANGELOG.md only. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk because this PR only changes documentation (`CHANGELOG.md`) and does not modify runtime code. > > **Overview** > Updates `CHANGELOG.md` *[Unreleased]* to include missing Pro release notes for PR `#3069`, documenting the new `renderer_http_keep_alive_timeout` config and the associated fix for SSR failures from stale persistent HTTP/2 connections to the node renderer. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 8d1a480. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> * **New Features** * Interactive mode selection prompt for CLI tool when no explicit mode is specified * Configurable keep-alive timeout setting for Pro users (default 30 seconds) * **Bug Fixes** * Enhanced validation and error handling for invalid request payloads * Improved template literal handling in code generation * Better HTTP connection stability with enhanced diagnostic messaging <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Fixes #3071
Problem
Two related errors appear in production node renderer logs:
Error 1:
FST_ERR_CTP_INVALID_CONTENT_LENGTH{ "level": "error", "msg": "Unhandled Fastify error", "err": { "code": "FST_ERR_CTP_INVALID_CONTENT_LENGTH", "message": "Request body size did not match Content-Length", "statusCode": 400 } }Error 2:
INVALID NIL or NULL result for rendering{ "level": "error", "msg": "INVALID result for prepareResult\n\nJS code for rendering request was:\nnull\n\nEXCEPTION MESSAGE:\nINVALID NIL or NULL result for rendering" }Root Cause Analysis
These two errors are causally linked — Error 1 triggers Error 2.
How the render request normally works
renderingRequest(the JavaScript code to execute in the SSR VM), optional bundle files, and metadata fieldsContent-Lengthheader, and attaches parsed fields toreq.bodyreq.body.renderingRequestand passes it to the VM for server-side renderingWhat goes wrong
The Ruby gem uses HTTPX with persistent HTTP/2 connections to the node renderer. HTTP/2 connections are long-lived and multiplexed, but they can become stale — the node renderer may close its end of the connection (due to idle timeout, restart, or resource pressure) while the Ruby side still considers it active.
When the Ruby gem writes a render request into a stale connection:
Content-Lengthheader promisedFST_ERR_CTP_INVALID_CONTENT_LENGTH(Error 1)renderingRequestis never attached toreq.bodyreq.body.renderingRequestasundefined/null, which propagates intoprepareResult()→runInVM(null, ...)→ the VM returns null → "INVALID NIL or NULL result for rendering" (Error 2)The confusing part for operators is that Error 2 looks like a JavaScript/rendering bug ("JS code for rendering request was: null"), when the real problem is a transport-layer issue that happened before any JavaScript executed.
Changes
1. Prevent stale connections (root cause fix)
Ruby gem (
request.rb,configuration.rb): Addkeep_alive_timeout(default: 30s) to the HTTPX persistent connection configuration. This tells HTTPX to close idle connections after 30 seconds, preventing the Ruby side from writing into connections that the node renderer has already closed. Configurable viarenderer_http_keep_alive_timeoutwith validation (must be a positive number or nil).2. Early validation with actionable diagnostics (defense in depth)
Node renderer (
worker.ts): ValidaterenderingRequestimmediately after body parsing, before entering the render pipeline. When the field is missing, null, or empty, the renderer now returns a descriptive 400 error instead of letting null propagate through the VM:This gives operators immediate insight into what happened and where to look, rather than the misleading "INVALID NIL or NULL result for rendering" message.
3. Specific Content-Length mismatch logging
Node renderer (
worker.ts): The FastifyonErrorhook now detectsFST_ERR_CTP_INVALID_CONTENT_LENGTHspecifically and logs it as "Invalid request body framing" with an actionable hint about client/proxy truncation, rather than the generic "Unhandled Fastify error" message.4. Security: expanded sensitive key filtering
Node renderer (
worker.ts): The diagnostic message includes body keys to help debugging, but filters out sensitive field names. Expanded from justpasswordto also filtertoken,secret,api_key,auth_token,authorization, andcredentials— since these diagnostics flow through error reporters (Sentry, Honeybadger).Test plan
renderingRequest, and expanded sensitive key filteringrenderer_http_keep_alive_timeoutvalidation🤖 Generated with Claude Code
Note
Medium Risk
Touches the Rails↔Node renderer transport configuration (HTTPX timeouts) and request validation/logging, which can affect SSR reliability and performance if misconfigured, but is scoped and covered by tests.
Overview
Fixes SSR failures caused by stale persistent connections by adding a new Ruby config
renderer_http_keep_alive_timeout(default30) and wiring it into HTTPXkeep_alive_timeout(with validation and improved connection error context).Hardens the Node renderer against truncated/malformed multipart bodies by returning an actionable
400whenrenderingRequestis missing/null/empty/array, refining the reported type for empty strings, and filtering additional sensitive body keys (includingcredentials) from diagnostics; corresponding tests are updated/added.Reviewed by Cursor Bugbot for commit 818020c. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Bug Fixes
Tests