Skip to content

Add configurable host option to node renderer Fastify worker#2585

Merged
justin808 merged 9 commits intomasterfrom
jg/579-fastify-listen-host
Mar 11, 2026
Merged

Add configurable host option to node renderer Fastify worker#2585
justin808 merged 9 commits intomasterfrom
jg/579-fastify-listen-host

Conversation

@justin808
Copy link
Copy Markdown
Member

@justin808 justin808 commented Mar 11, 2026

Summary

  • Adds a host config option to the node renderer's Fastify listen call (default: 'localhost', env: RENDERER_HOST)
  • Fastify defaults to localhost/127.0.0.1, which blocks external health checks in containerized environments (Docker, ECS with ALB)
  • The previous Express-based worker (3.x) defaulted to 0.0.0.0, so this restores configurability

Fixes shakacode/react_on_rails_pro#579

Test plan

  • Verify build passes (pnpm run build)
  • Verify lint passes (pnpm run lint)
  • Test with host: '0.0.0.0' in container environment to confirm health checks work
  • Test default behavior (no host set) still binds to localhost

🤖 Generated with Claude Code


Note

Low Risk
Small change limited to server bind configuration; default behavior remains localhost, with risk mainly around misconfiguration exposing the renderer on unintended interfaces.

Overview
Adds a new host option to the node renderer configuration (defaulting to localhost and overridable via RENDERER_HOST).

Updates the Fastify worker to pass { host, port } to app.listen() and to include the bound address in startup logs, enabling scenarios like binding to 0.0.0.0 for external health checks.

Written by Cursor Bugbot for commit 84b2b7b. Configure here.

Summary by CodeRabbit

  • New Features

    • Renderer can be configured to bind to a specific host (defaults to process.env.RENDERER_HOST or 'localhost'); startup and logs now display host:port.
  • Documentation

    • Added host configuration guidance, including using 0.0.0.0 for containerized deployments and a security note about exposing network interfaces (recommend private networking, firewalls/ALB rules, or authentication).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a public host configuration (default from RENDERER_HOST or 'localhost') and uses it when starting the Fastify worker so the renderer binds to the configured host and port.

Changes

Cohort / File(s) Summary
Configuration Definition
packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts
Adds host: string to the exported Config interface; sets default host to `env.RENDERER_HOST
Server Implementation
packages/react-on-rails-pro-node-renderer/src/worker.ts
getConfig() now provides host; app.listen() is called with { port, host }; startup log updated to show host:port.
Documentation
docs/pro/node-renderer/js-configuration.md, CHANGELOG.md
Documents the new host option, defaulting to `process.env.RENDERER_HOST

Sequence Diagram(s)

sequenceDiagram
  participant Starter as Starter
  participant Config as ConfigBuilder
  participant Server as FastifyServer
  participant Logger as Logger

  Starter->>Config: getConfig()
  Config-->>Starter: config { port, host, ... }
  Starter->>Server: app.listen({ port, host })
  Server-->>Logger: onListening(host, port)
  Logger-->>Starter: log "listening on host:port"
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I hopped through configs, found a spot,
Added host so bind's not forgot.
From localhost or open shore,
Containers ping and servers roar—
🥕 a tiny hop, outputs a lot!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding a configurable host option to the node renderer Fastify worker, which is the core objective.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #579: adds host configuration field, sets default to localhost with RENDERER_HOST override, binds with host:port in listen call, and updates startup logging.
Out of Scope Changes check ✅ Passed All changes are scoped to the host configuration requirement: config interface, default values, worker binding, documentation, and changelog entries—no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jg/579-fastify-listen-host

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 and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 11, 2026

Greptile Summary

This PR adds a configurable host binding option to the Fastify-based node renderer worker, defaulting to 'localhost' (overridable via RENDERER_HOST env var). The change restores parity with the previous Express-based worker's 0.0.0.0 flexibility and unblocks external health checks in containerized environments (Docker, ECS with ALB).

Key changes:

  • Config interface gains a host: string field with inline documentation
  • defaultConfig reads RENDERER_HOST env var, falling back to 'localhost'
  • worker.ts threads host into app.listen({ port, host }) and updates the startup log line to host:port

Issues found:

  • RENDERER_HOST is not included in the envValuesUsed() function, so it will not appear in the startup configuration log output — a minor observability gap compared to every other env-backed setting
  • The Config interface carries a comment reminding contributors to update ./docs/node-renderer/js-configuration.md when the interface changes; the docs file does not appear to be updated in this PR

Confidence Score: 4/5

  • This PR is safe to merge; the change is minimal, well-scoped, and the default preserves existing behaviour.
  • The logic change is small and correct — host is plumbed through cleanly from config to Fastify's listen call. The only gaps are a missing envValuesUsed() entry (affects startup-log observability, not runtime behaviour) and an un-updated docs file. Neither blocks correctness.
  • packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts — missing RENDERER_HOST in envValuesUsed() and docs not updated.

Important Files Changed

Filename Overview
packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts Adds host: string to the Config interface and sets its default from RENDERER_HOST env var (falls back to 'localhost'). Minor issue: RENDERER_HOST is not tracked in envValuesUsed(), so it won't appear in startup logs; and the co-located docs reminder was not followed.
packages/react-on-rails-pro-node-renderer/src/worker.ts Destructures host from getConfig() and threads it into app.listen({ port, host }). Log message updated to show host:port. The change is minimal and correct.

Sequence Diagram

sequenceDiagram
    participant Operator as Operator / Container Env
    participant Config as configBuilder.ts
    participant Worker as worker.ts
    participant Fastify as Fastify Server

    Operator->>Config: Set RENDERER_HOST (e.g. 0.0.0.0) or leave default
    Config->>Config: defaultConfig.host = env.RENDERER_HOST || 'localhost'
    Worker->>Config: buildConfig(userConfig)
    Config-->>Worker: getConfig() → { port, host, ... }
    Worker->>Fastify: app.listen({ port, host })
    Fastify-->>Worker: Callback: listening on host:port
    Worker->>Worker: log.info("Node renderer … listening on host:port!")

    Note over Operator,Fastify: With host='0.0.0.0', external health checks<br/>(ALB, Docker) can reach the Fastify server
Loading

Comments Outside Diff (2)

  1. packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts, line 200-225 (link)

    RENDERER_HOST missing from envValuesUsed()

    Every other env-variable-backed config field is tracked in envValuesUsed() so it appears in the startup log under "ENV values used for settings". The new RENDERER_HOST variable is absent, meaning operators who set it won't see it reported in the startup output — making it harder to confirm the binding address in production/containerized deployments (exactly the environments this feature targets).

  2. packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts, line 23 (link)

    Docs file not updated for new host option

    The comment on this line explicitly instructs contributors to update ./docs/node-renderer/js-configuration.md whenever the Config interface changes. A new host field (with its RENDERER_HOST env var) was added but the documentation file does not appear to have been updated in this PR.

Last reviewed commit: 84b2b7b

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

test comment please ignore

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but it could not run because Privacy Mode (Legacy) is turned on. To enable Bugbot Autofix, switch your privacy mode in the Cursor dashboard.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

PR Review: The implementation is clean and correct — passing { port, host } to Fastify app.listen() is the right approach, and the default of localhost preserves existing behavior safely. Two issues: (1) Missing documentation update — configBuilder.ts line 23 has a comment reminding contributors to update js-configuration.md when config changes. The new host option backed by RENDERER_HOST was not documented there. (2) Missing CHANGELOG entry — this is a new user-facing configuration option that warrants an entry under Unreleased > Added in CHANGELOG.md.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 11, 2026

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.63 KB (0%)
react-on-rails/client bundled (gzip) (time) 62.63 KB (0%)
react-on-rails/client bundled (brotli) 53.74 KB (0%)
react-on-rails/client bundled (brotli) (time) 53.74 KB (0%)
react-on-rails-pro/client bundled (gzip) 63.63 KB (0%)
react-on-rails-pro/client bundled (gzip) (time) 63.63 KB (0%)
react-on-rails-pro/client bundled (brotli) 54.62 KB (0%)
react-on-rails-pro/client bundled (brotli) (time) 54.62 KB (0%)
registerServerComponent/client bundled (gzip) 127.44 KB (0%)
registerServerComponent/client bundled (gzip) (time) 127.44 KB (0%)
registerServerComponent/client bundled (brotli) 61.6 KB (0%)
registerServerComponent/client bundled (brotli) (time) 61.6 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) 122.02 KB (0%)
wrapServerComponentRenderer/client bundled (gzip) (time) 122.02 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) 56.77 KB (0%)
wrapServerComponentRenderer/client bundled (brotli) (time) 56.77 KB (0%)

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

PR Review

The implementation is clean and correct — passing { port, host } to Fastify's app.listen() is the right approach, and the default of 'localhost' preserves existing behavior safely.

Two issues to address:

Missing documentation update

configBuilder.ts has an explicit instruction at line 23:

/* Update ./docs/node-renderer/js-configuration.md when something here changes */

The js-configuration.md file enumerates every config option with its env var, but the new host option (backed by RENDERER_HOST) was not added. A line like the following should appear after the port entry:

1. **host** (default: `process.env.RENDERER_HOST || 'localhost'`) - The host/IP address the renderer binds to. Set to `0.0.0.0` in containerized environments (Docker, ECS with ALB) where external health checks need to reach the server.

Missing CHANGELOG entry

This is a new user-facing configuration option. It warrants an entry under [Unreleased] > Added in CHANGELOG.md.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts`:
- Around line 150-151: env.RENDERER_HOST is used for the host value but
envValuesUsed() and therefore logSanitizedConfig() don't include RENDERER_HOST,
so startup diagnostics can't show if the bind address came from the environment;
update envValuesUsed() to include 'RENDERER_HOST' (and any matching key name
used in config) so logSanitizedConfig() will report whether host was set from
env.RENDERER_HOST or the default, referencing the host property and
env.RENDERER_HOST in
packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts and
ensuring envValuesUsed() and logSanitizedConfig() are updated consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 11678e41-ffee-4d1a-8b3f-a10cc3dd9ca9

📥 Commits

Reviewing files that changed from the base of the PR and between fe60b1e and 84b2b7b.

📒 Files selected for processing (2)
  • packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts
  • packages/react-on-rails-pro-node-renderer/src/worker.ts

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

Review

The implementation is clean, correct, and addresses all the concerns raised by earlier reviewers. Specifically:

  • host field added to Config interface with clear inline docs
  • ✅ Default sourced from RENDERER_HOST env var with safe 'localhost' fallback
  • envValuesUsed() now includes RENDERER_HOST (startup diagnostics will report the configured bind address)
  • app.listen({ port, host }) is the correct Fastify API
  • ✅ Documentation updated in js-configuration.md

One remaining gap: no CHANGELOG entry

This is a new user-facing configuration option for the Pro node renderer. Per the project's changelog guidelines, it should have an entry under [Unreleased] > #### Pro > Added in CHANGELOG.md, e.g.:

- **Configurable renderer host binding (Pro)**: Added `host` config option (default: `'localhost'`, env: `RENDERER_HOST`) to the Node Renderer Fastify worker. Set to `0.0.0.0` in containerized environments (Docker, ECS with ALB) to allow external health checks. [PR 2585](https://github.com/shakacode/react_on_rails/pull/2585) by [justin808](https://github.com/justin808).

Otherwise ready to merge once the CHANGELOG entry is added.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/pro/node-renderer/js-configuration.md`:
- Around line 15-16: Update the host guidance to include a security warning:
after the "host" (process.env.RENDERER_HOST || 'localhost') explanation, add a
short caution that binding to 0.0.0.0 exposes the renderer on all interfaces and
should only be used when protected by private networking, firewall/ALB rules, or
when authentication (the password config) is enabled; reference the "host" and
"password" settings so readers know which config keys to secure and recommend
alternatives (localhost or loopback) if those protections are not in place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ec705949-d1e9-47ad-86f4-0c97c2b38ace

📥 Commits

Reviewing files that changed from the base of the PR and between 84b2b7b and 91f5f47.

📒 Files selected for processing (2)
  • docs/pro/node-renderer/js-configuration.md
  • packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts

Comment thread docs/pro/node-renderer/js-configuration.md
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

Overall this is a clean, well-scoped change. The default of 'localhost' preserves backward compatibility, the docs include a good security caution about 0.0.0.0 exposure, and the envValuesUsed() reporting is consistent with existing patterns.

Issue: Log message shows configured host, not actual bound address

In worker.ts, the app.listen callback form receives (err, address) as arguments where address is the fully-resolved string Fastify actually bound to. The current code ignores those callback params and logs the configured host:port values instead. This can be misleading when 'localhost' resolves to IPv6 (e.g. [::1]:3800). Using the address argument from the callback would make the log accurate regardless of how the OS resolves the hostname.

Minor: No validation of the host string

Invalid values (e.g. RENDERER_HOST=not-a-valid-host) will fail at runtime when Fastify tries to bind. Consistent with how port is handled, so acceptable — the failure is fast and visible at startup.

Minor: envValuesUsed() falsy check

!userConfig.host would also be truthy for an explicitly empty string host, causing the env variable to be reported as in use when it is not. Same pre-existing pattern as port, not a new issue introduced here.

app.listen({ port, host }, () => {
const workerName = worker ? `worker #${worker.id}` : 'master (single-process)';
log.info(`Node renderer ${workerName} listening on port ${port}!`);
log.info(`Node renderer ${workerName} listening on ${host}:${port}!`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The log uses the configured host value, but Fastify's callback receives (err, address) where address is the actual resolved address (e.g. http://[::1]:3800 when 'localhost' resolves to IPv6). Consider using the callback argument instead so the log always reflects the true bound address:

Suggested change
log.info(`Node renderer ${workerName} listening on ${host}:${port}!`);
log.info(`Node renderer ${workerName} listening on ${address}!`);

(The callback signature is (err: Error | null, address: string) => void, so address is already available.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in commit 1b36cd3.

Updated the callback to use (err, address) — now logs the actual bound address from Fastify and exits with an error log if the listen fails.

@justin808
Copy link
Copy Markdown
Member Author

Addressed in commit 4659dcf.

Added a new Pro changelog entry under Unreleased -> Pro -> Added in CHANGELOG.md for the configurable node-renderer host option (RENDERER_HOST / default localhost), including containerized 0.0.0.0 guidance.

This addresses the missing changelog feedback noted in:

justin808 and others added 4 commits March 10, 2026 17:36
Fastify defaults to listening on localhost (127.0.0.1), which breaks
external health checks in containerized environments (Docker, ECS).
Add a `host` config option (default: 'localhost', env: RENDERER_HOST)
so users can set '0.0.0.0' for container deployments.

Fixes shakacode/react_on_rails_pro#579

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@justin808 justin808 force-pushed the jg/579-fastify-listen-host branch from 4659dcf to a9fe5b0 Compare March 11, 2026 03:37
const { worker } = cluster;
if (workersCount === 0 || cluster.isWorker) {
app.listen({ port }, () => {
app.listen({ port, host }, () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The callback passed to app.listen ignores its arguments. In Fastify v5 the callback signature is (err: Error | null, address: string) => void. With the new host option, an invalid or unavailable bind address will now fail silently — no error is thrown or logged.

Consider either handling the error in the callback or switching to the promise form (which throws on failure and can be caught with try/catch):

Suggested change
app.listen({ port, host }, () => {
app.listen({ port, host }, (err, address) => {
if (err) {
log.error({ err }, `Node renderer failed to start`);
process.exit(1);
}
const workerName = worker ? `worker #${worker.id}` : 'master (single-process)';
log.info(`Node renderer ${workerName} listening on ${address}!`);
});

Using address from the callback is also more accurate than interpolating the configured host/port strings, since Fastify resolves localhost127.0.0.1 before binding and the callback receives the real bound address.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in commit 1b36cd3.

Added error handling in the app.listen callback — on failure it now logs the error and exits. Also switched to using the callback's address argument for the startup log, which reflects the true bound address.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

Overall this is a clean, minimal, well-motivated change. The security documentation in the docs page is a nice touch.

What looks good

  • Consistent with the port pattern — the host field follows exactly the same default/env-override/diagnostic pattern already used for port, making it easy to understand.
  • Safe default — keeping 'localhost' as the default preserves existing behavior and avoids accidental exposure on upgrade.
  • Security caution in docs — the warning about 0.0.0.0 exposing all interfaces is important and well-placed.
  • Log improvement — showing host:port instead of just port is strictly more useful.

One issue worth addressing

Left an inline comment on the app.listen callback (line 465 of worker.ts). The callback ignores its err argument, which is a pre-existing gap, but it becomes more relevant now that users can supply arbitrary hosts. An invalid or un-bindable host will fail silently. The fix is straightforward — see the suggestion on the inline comment.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/react-on-rails-pro-node-renderer/src/worker.ts (1)

465-467: Add a regression test for host-aware listen binding.

Lines 465-467 correctly bind to { host, port }, but current test patterns like packages/react-on-rails-pro-node-renderer/tests/streamErrorHang.test.ts using only { port: 0 } won’t catch future regressions on host propagation. Please add one integration test that asserts worker startup honors a non-default host.

I can draft a minimal test case for this if you want.

🤖 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/src/worker.ts` around lines 465 -
467, Add an integration test that verifies the server binds to a non-default
host by exercising the same code path that calls app.listen({ host, port }) in
worker.ts (the block that constructs workerName and calls app.listen). Create a
new test (e.g., tests/hostBind.test.ts) that starts the renderer in-process or
via the same startup helper used by other tests, passing a non-default host (for
example '127.0.0.1' or 'localhost') and port: 0, then assert the running
server's bound address equals the requested host (inspect the server instance
returned by app.listen or the address reported by the running process). Use the
existing test harness patterns from
packages/react-on-rails-pro-node-renderer/tests/* to start/stop the worker
cleanly and make sure the test fails if host propagation to app.listen is
broken.
🤖 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/src/worker.ts`:
- Around line 465-467: Add an integration test that verifies the server binds to
a non-default host by exercising the same code path that calls app.listen({
host, port }) in worker.ts (the block that constructs workerName and calls
app.listen). Create a new test (e.g., tests/hostBind.test.ts) that starts the
renderer in-process or via the same startup helper used by other tests, passing
a non-default host (for example '127.0.0.1' or 'localhost') and port: 0, then
assert the running server's bound address equals the requested host (inspect the
server instance returned by app.listen or the address reported by the running
process). Use the existing test harness patterns from
packages/react-on-rails-pro-node-renderer/tests/* to start/stop the worker
cleanly and make sure the test fails if host propagation to app.listen is
broken.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 628136c7-6440-4e88-8a45-cd800f98c68b

📥 Commits

Reviewing files that changed from the base of the PR and between 4659dcf and a9fe5b0.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • docs/pro/node-renderer/js-configuration.md
  • packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts
  • packages/react-on-rails-pro-node-renderer/src/worker.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/pro/node-renderer/js-configuration.md
  • packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts

Add error handling to the Fastify app.listen callback so invalid or
unavailable bind addresses fail loudly instead of silently. Use the
callback's `address` argument for the startup log, which reflects the
true bound address (e.g. resolved IPv6) rather than the configured
host/port strings.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
app.listen({ port }, () => {
app.listen({ port, host }, (err, address) => {
if (err) {
log.error({ err }, `Node renderer failed to start`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The error message doesn't include the address being bound to, which makes it harder to diagnose the root cause (e.g. invalid host value, port already in use, permission denied). Consider logging the attempted host/port:

Suggested change
log.error({ err }, `Node renderer failed to start`);
log.error({ err, host, port }, `Node renderer failed to start on ${host}:${port}`);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in commit 281993e. The error log now includes host and port in both the structured fields and the message string.

Comment thread packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

Review

Overall this is a clean, well-scoped change. The default of localhost is safe (preserves existing behavior), the security documentation is clear, and the improved error handling in the app.listen() callback is a nice bonus fix.

Strengths

  • Safe default: 'localhost' keeps existing bind behavior unchanged for all current deployments.
  • Security docs: The caution about 0.0.0.0 exposure and mitigations (private networking, firewall/ALB, password auth) are clearly documented.
  • Error handling improvement: The new (err, address) callback correctly handles startup failures with process.exit(1) — the old callback silently dropped errors.
  • Logging improvement: Using address from the callback instead of interpolating port means the logged address reflects what Fastify actually bound to (including the resolved host).

Minor issues

  1. Error message missing context (inline on worker.ts:467): When startup fails, the log does not include which host/port was attempted, making it harder to triage a misconfigured RENDERER_HOST. Suggestion left inline.

  2. No startup warning for non-localhost without password: If a user sets host: '0.0.0.0' but has not configured password, the renderer is network-accessible with no authentication. A log.warn(...) at startup would reinforce the security note in the docs and catch misconfiguration early.

  3. Falsy fallback behavior (inline on configBuilder.ts:150, informational): RENDERER_HOST='' silently falls through to 'localhost'. Not a bug, just worth knowing — consistent with how RENDERER_PORT is handled.

None of these block merging — items 1 and 3 are minor and item 2 is a nice-to-have.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

Overall this is a clean, well-scoped change. The default of localhost preserves current behaviour with zero risk to existing deployments; the security caution in the docs is clear and actionable.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

Overall this is a clean, well-scoped change. The default of localhost preserves current behaviour with zero risk to existing deployments; the security caution in the docs is clear and actionable.

Improvement: Adding error handling to the app.listen callback is a genuine improvement -- previously a bind failure would silently pass through. Logging the resolved address from the callback (rather than configured strings) also correctly handles IPv6 address formatting.

Minor -- envValuesUsed falsy check (see inline): The host: empty-string case is treated as not-set, consistent with the existing port pattern, but checking whether the host key exists in userConfig (same guard as supportModules/stubTimers) would be strictly correct.

Minor -- no host validation: An invalid host string now fails loudly thanks to the new process.exit(1) path, but an upfront format check would give a clearer error message before Fastify even attempts to bind.

Both are minor; the current implementation is functionally correct for all realistic inputs.

Comment thread packages/react-on-rails-pro-node-renderer/src/shared/configBuilder.ts Outdated
function envValuesUsed() {
return {
RENDERER_PORT: !userConfig.port && env.RENDERER_PORT,
RENDERER_HOST: !('host' in userConfig) && env.RENDERER_HOST,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The host check uses key-existence ('host' in userConfig) while every other field in this function uses a falsy check (e.g. !userConfig.port). This is actually more semantically correct — it means an explicit host: '' still suppresses the env-var report — but it creates a visible inconsistency and the test below only covers the empty-string edge case rather than the common case.

Consider either:

  1. Aligning this with the existing pattern: !userConfig.host && env.RENDERER_HOST (accepts the same port-zero-style edge case the rest of the function already has), or
  2. Keeping in here and adding a comment explaining why this check is intentionally different.

As-is, a reader maintaining the codebase may "fix" it to match the rest of the file and inadvertently change the behaviour.

it('does not mark RENDERER_HOST as env-provided when host key exists in user config', () => {
process.env.RENDERER_HOST = '0.0.0.0';

const envValues = envValuesUsedForRenderedConfig({ host: '' });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The test passes { host: '' } — an empty string is an invalid bind address, so this is testing a somewhat artificial edge case. The intent is to verify that any explicit host key in userConfig (even a non-address value) suppresses env reporting. That intent would be clearer with a valid host value, and it would be even clearer if the test name reflected what is actually being asserted:

Suggested change
const envValues = envValuesUsedForRenderedConfig({ host: '' });
const envValues = envValuesUsedForRenderedConfig({ host: '127.0.0.1' });

Also worth adding a third test for the fully-default path (no RENDERER_HOST env var, no host key in userConfig → effective host is 'localhost'), which isn't covered at all right now.

fatal: jest.fn(),
},
}));
const { buildConfig, logSanitizedConfig } = jest.requireActual('../src/shared/configBuilder');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

jest.requireActual loads the actual configBuilder implementation (bypassing any auto-mock of the module itself), but because jest.doMock was already registered for ../src/shared/log before this call, Jest resolves that dependency through the mock registry when configBuilder is freshly loaded — so the mock does take effect.

This pattern is established elsewhere in the test suite (e.g. licenseValidator.test.ts), so it's fine, but it's non-obvious. A brief inline comment here would help future readers understand why requireActual is used instead of require or requireMock.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

Overall this is a clean, well-scoped change. The core implementation is solid: error handling in the app.listen callback is a genuine improvement over the previous fire-and-forget pattern, structured Pino log fields follow best practices, and using the resolved address from the callback for the success log correctly handles IPv6 address resolution. The security caution in the docs is appropriate.

envValuesUsed() inconsistency (configBuilder.ts line 203): RENDERER_HOST uses the in operator (key existence) while all other fields use falsy checks like !userConfig.port. This is more semantically correct for host, but inconsistent with the surrounding code, and the accompanying test only exercises an empty-string edge case. See inline comment.

Test coverage gaps: (1) The test passing host as an empty string uses an invalid bind address; a real value like 127.0.0.1 makes the intent clearer. (2) There is no test for the fully-default path (no RENDERER_HOST env var, no host key in userConfig -> defaults to localhost). (3) The jest.requireActual + jest.doMock pattern is correct and matches licenseValidator.test.ts, but a short comment on why requireActual is used would help future readers.

No host validation (nice-to-have): an invalid host from an env var typo will reach app.listen without early feedback. The new error handler in worker.ts catches this at startup and exits, which is acceptable. A guard in buildConfig similar to the maxVMPoolSize check would give a faster and more descriptive error, but this is optional.

None of these block merging - the feature is correct and the conservative default of localhost is safe.

@justin808 justin808 merged commit aff3b88 into master Mar 11, 2026
49 checks passed
@justin808 justin808 deleted the jg/579-fastify-listen-host branch March 11, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement P2 Backlog priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Node Renderer Fastify worker binds to localhost, breaking container health checks

1 participant