Skip to content

Make Render health checks immediate and deterministic - #19

Merged
Occumed79 merged 3 commits into
mainfrom
fix/render-health-timeout
Jul 26, 2026
Merged

Make Render health checks immediate and deterministic#19
Occumed79 merged 3 commits into
mainfrom
fix/render-health-timeout

Conversation

@Occumed79

@Occumed79 Occumed79 commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Render built and started the service, but its internal /health probe timed out before deployment promotion.

This change:

  • handles /health and /healthz before URL parsing, filesystem access, or map routing;
  • returns a minimal 200 ok response for both GET and HEAD;
  • closes the health-check connection explicitly;
  • binds through the configured host with 0.0.0.0 as the safe default;
  • removes the server response's dependency on response.req;
  • adds an automated post-build test using a Render-style Host header so deployment readiness is validated in CI.

Summary by CodeRabbit

  • New Features

    • Added support for health checks at /health and /healthz.
    • Health checks now support both GET and HEAD requests with standard responses.
    • Improved compatibility with hosted environments, proxies, and forwarded host information.
    • Added CORS preflight handling and clearer method validation.
  • Bug Fixes

    • Improved static asset caching behavior.
    • Added more reliable server error handling and shutdown behavior.
  • Tests

    • Builds now verify server health automatically, including health response status and content.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Occumed79 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The server now supports host configuration, GET/HEAD health responses, centralized routing, static cache policies, timeouts, and guarded errors. A new health-check script probes the running server, and the build executes it after existing checks.

Changes

Server health verification

Layer / File(s) Summary
HTTP response contracts
server.mjs
Server responses now account for request methods, expose explicit health headers and bodies, resolve forwarded hosts, and apply updated style and forbidden-response behavior.
Request routing and lifecycle
server.mjs
Routing handles health paths, CORS preflight, allowed methods, static caching, server errors, timeouts, and configurable host binding.
Build health probe
scripts/check-server-health.mjs, package.json
The build starts a server, validates GET and HEAD health responses, terminates the process, and runs this verification through check:server.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant npmBuild
  participant checkServerHealth
  participant server.mjs
  participant HealthEndpoint
  npmBuild->>checkServerHealth: run check:server
  checkServerHealth->>server.mjs: spawn with HOST and port
  server.mjs-->>checkServerHealth: Health endpoint ready
  checkServerHealth->>HealthEndpoint: GET /health?render-probe=1
  HealthEndpoint-->>checkServerHealth: 200 and ok
  checkServerHealth->>HealthEndpoint: HEAD /health?render-probe=1
  HealthEndpoint-->>checkServerHealth: 200 and empty body
  checkServerHealth->>server.mjs: send SIGTERM
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: making Render health checks immediate and deterministic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/render-health-timeout

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.

@Occumed79
Occumed79 merged commit 62bf5f2 into main Jul 26, 2026
1 of 2 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b5c559e11

ℹ️ 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".

Comment thread package.json
"check": "npm run check:export && npm run check:runtime",
"dev": "npm run prepare:assets && vite",
"build": "npm run prepare:assets && npm run check && vite build",
"build": "npm run prepare:assets && npm run check && vite build && npm run check:server",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Invoke the health test from the validation workflow

The checked GitHub Actions pipeline still runs npx vite build directly in .github/workflows/validate.yml:56-57, so it never executes this newly appended npm run check:server. Consequently, pull requests can pass CI even when the health endpoint test fails, contrary to the intended CI readiness validation; update that workflow to call this build script or run check:server explicitly.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
server.mjs (2)

173-176: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider exiting/closing on fatal server errors.

Setting process.exitCode = 1 only affects the exit code if/when the process naturally terminates. If error fires after the server is already listening (e.g., EMFILE during accept()), the server keeps running and accepting connections in a degraded state rather than actually stopping — which could let Render's health probe keep succeeding against a partially broken process.

♻️ Proposed fix: explicitly stop the server on fatal errors
 server.on('error', (error) => {
   console.error('Occu-Med Map server error:', error);
   process.exitCode = 1;
+  server.close();
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.mjs` around lines 173 - 176, Update the server error handler
registered by server.on('error') to explicitly stop or close the server when a
fatal error occurs, while preserving the error log and nonzero exit status.
Ensure the shutdown path prevents the server from continuing to accept
connections after errors such as EMFILE.

26-31: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Set PUBLIC_ORIGIN instead of trusting forwarded headers.

PUBLIC_ORIGIN avoids sourcing the runtime origin from client-supplied x-forwarded-* headers; rely only on Host or platform-trusted headers when that env var is unset.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.mjs` around lines 26 - 31, Update the origin-building function around
forwardedProtocol and forwardedHost to prefer the PUBLIC_ORIGIN environment
variable and return it when set. When PUBLIC_ORIGIN is unset, stop using
client-supplied x-forwarded-proto and x-forwarded-host headers; derive the
origin from the request Host header (or the existing localhost fallback), using
only platform-trusted headers if explicitly available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server.mjs`:
- Around line 169-171: Update the timeout assignments near server initialization
so headersTimeout is less than or equal to requestTimeout, preserving the
intended timeout ordering and ensuring the configured values satisfy Node’s
invariant.

---

Nitpick comments:
In `@server.mjs`:
- Around line 173-176: Update the server error handler registered by
server.on('error') to explicitly stop or close the server when a fatal error
occurs, while preserving the error log and nonzero exit status. Ensure the
shutdown path prevents the server from continuing to accept connections after
errors such as EMFILE.
- Around line 26-31: Update the origin-building function around
forwardedProtocol and forwardedHost to prefer the PUBLIC_ORIGIN environment
variable and return it when set. When PUBLIC_ORIGIN is unset, stop using
client-supplied x-forwarded-proto and x-forwarded-host headers; derive the
origin from the request Host header (or the existing localhost fallback), using
only platform-trusted headers if explicitly available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03d2eacc-9cb6-458c-80ce-666c49314592

📥 Commits

Reviewing files that changed from the base of the PR and between 899327c and 1b5c559.

📒 Files selected for processing (3)
  • package.json
  • scripts/check-server-health.mjs
  • server.mjs

Comment thread server.mjs
Comment on lines +169 to +171
server.requestTimeout = 30_000;
server.headersTimeout = 35_000;
server.keepAliveTimeout = 5_000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Node.js http server headersTimeout vs requestTimeout: does assigning both as instance properties after createServer bypass the ERR_OUT_OF_RANGE validation enforced in the constructor?

💡 Result:

No, assigning headersTimeout and requestTimeout as instance properties after the http.Server instance has been created does not trigger the ERR_OUT_OF_RANGE validation enforced in the constructor [1]. The ERR_OUT_OF_RANGE validation for these timeouts is part of the internal server option storage logic, which executes specifically during the construction of the http.Server instance (via http.createServer or new http.Server) [1][2][3]. Once the instance is initialized, these properties are exposed on the object, and you can manually overwrite them [4]. Node.js does not re-validate these instance properties when they are modified directly at runtime [1]. While it is technically possible to bypass the constructor's validation by assigning values after initialization, doing so is discouraged as it may lead to inconsistent server behavior or unexpected connection timeouts [4][5]. Node.js relies on these values to manage connection lifetimes and mitigate potential Denial-of-Service attacks, and the internal logic expects that headersTimeout is less than or equal to requestTimeout [4][6][1]. If you set them to invalid values, the server's internal connection-tracking mechanisms may not operate as intended [6][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Node runtime availability:\n'
node --version

printf '\nInspect relevant server.mjs lines:\n'
if [ -f server.mjs ]; then
  nl -ba server.mjs | sed -n '150,180p'
else
  fd -i '^server\.mjs$' -x sh -c 'echo "--- $1"; nl -ba "$1" | sed -n "150,180p"' sh {}
fi

printf '\nCheck Node http.Server behavior for constructor validation and post-init property assignment:\n'
node - <<'JS'
const http = require('http');

console.log('--- constructor with requestTimeout=30000, headersTimeout=35000 ---');
try {
  const bad = http.createServer({ requestTimeout: 30000, headersTimeout: 35000 });
  console.log('created:', bad.headersTimeout, bad.requestTimeout);
} catch (err) {
  console.log(err.name, err.code, err.message);
}

console.log('\n--- post-init assignment with same values ---');
const good = http.createServer({ requestTimeout: 30000, headersTimeout: 35000 });
good.headersTimeout = 35000;
good.requestTimeout = 30000;
console.log('created with same values:', good.headersTimeout, good.requestTimeout);

good.headersTimeout = 35000;
console.log('after post-init headersTimeout=', good.headersTimeout);
good.requestTimeout = 30000;
console.log('after post-init requestTimeout=', good.requestTimeout);

Repository: Occumed79/Map

Length of output: 263


🏁 Script executed:

#!/bin/bash
set -u

printf 'Inspect relevant server.mjs lines:\n'
if [ -f server.mjs ]; then
  sed -n '150,180p' server.mjs | cat -n
else
  for f in $(fd -i '^server\.mjs$' . 2>/dev/null); do
    echo "--- $f"
    sed -n '150,180p' "$f" | cat -n
  done
fi

printf '\nCheck Node http.Server behavior for constructor validation and post-init property assignment:\n'
node - <<'JS'
const http = require('http');

console.log('--- constructor with requestTimeout=30000, headersTimeout=35000 ---');
try {
  const bad = http.createServer({ requestTimeout: 30000, headersTimeout: 35000 });
  console.log('created:', bad.headersTimeout, bad.requestTimeout);
} catch (err) {
  console.log(err.name, err.code, err.message);
}

console.log('\n--- post-init assignment with same values ---');
const good = http.createServer({ requestTimeout: 30000, headersTimeout: 10000 });
good.headersTimeout = 35000;
good.requestTimeout = 30000;
console.log('initial headersTimeout=', good.headersTimeout, 'requestTimeout=', good.requestTimeout);
console.log('after post-init headersTimeout=', good.headersTimeout, 'requestTimeout=', good.requestTimeout);
JS

Repository: Occumed79/Map

Length of output: 1573


Keep headersTimeout below requestTimeout.

Assigning these after createServer bypasses Node’s constructor validation, but headersTimeout = 35000 still violates Node’s headersTimeout <= requestTimeout invariant with requestTimeout = 30000. The 35s header timeout can never fire before the 30s request timeout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.mjs` around lines 169 - 171, Update the timeout assignments near
server initialization so headersTimeout is less than or equal to requestTimeout,
preserving the intended timeout ordering and ensuring the configured values
satisfy Node’s invariant.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant