Skip to content

Instantly share code, notes, and snippets.

@Wxh16144
Created August 20, 2026 06:34
Show Gist options
  • Select an option

  • Save Wxh16144/7aa5eafae7b12d404bdf96c0c1297726 to your computer and use it in GitHub Desktop.

Select an option

Save Wxh16144/7aa5eafae7b12d404bdf96c0c1297726 to your computer and use it in GitHub Desktop.
one URL, two content types — a live HTML page for humans, plain JSON for machines (curl / uptime monitors / CI probes).
// ============================================================================
// Health endpoint: one URL, two content types — a live HTML page for humans,
// plain JSON for machines (curl / uptime monitors / CI probes).
//
// Why this pattern is neat:
// - ONE route, no separate /health and /health/ui. Content negotiation
// decides what the client gets based on the Accept header.
// - The browser gets a page that ticks in real time WITHOUT polling:
// the server injects the initial uptime once, then the client
// accumulates locally on requestAnimationFrame (smooth 60fps).
// Every 60s it silently re-fetches the JSON to re-anchor the baseline,
// correcting drift and noticing restarts.
// - curl / monitoring stays on the same URL and gets plain JSON —
// the payload is identical to what they got before this page existed.
//
// Prerequisites (install these BEFORE running):
// - Node.js >= 18 (uses built-in node:http only — NO npm install)
//
// Run:
// node server.js
//
// Try it:
// open http://localhost:3000/health -> live HTML page
// curl -s http://localhost:3000/health -> {"status":"ok","uptime":...}
// curl -s -H 'Accept: text/html' .../health -> the HTML page via curl
// ============================================================================
import { createServer } from 'node:http';
const PORT = Number(process.env.PORT ?? 3000);
/** Server start time (ms). */
const startedAt = Date.now();
/** Seconds the process has been alive (floored). */
function uptimeSeconds() {
return Math.floor((Date.now() - startedAt) / 1000);
}
/**
* The HTML page template. The server injects only the INITIAL uptime;
* everything after that is animated client-side.
*
* @param {number} uptime - initial uptime in seconds
* @returns {string} full HTML document
*/
function renderHealthHtml(uptime) {
return `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Health</title>
</head>
<body>
<pre id="t" style="font-size:large;font-variant-numeric:tabular-nums;">></pre>
<script>
// Base uptime in ms, injected by the server ONCE.
let base = ${uptime * 1000};
let startTs;
// Each frame: local elapsed (now - firstFrameTs) is added to the server
// baseline. No network traffic — the clock just runs.
function loop(ts) {
startTs ??= ts;
let ms = base + (ts - startTs);
const day = Math.floor(ms / (24 * 60 * 60 * 1000));
ms %= (24 * 60 * 60 * 1000);
const hour = Math.floor(ms / (60 * 60 * 1000));
ms %= (60 * 60 * 1000);
const min = Math.floor(ms / (60 * 1000));
ms %= (60 * 1000);
const sec = Math.floor(ms / 1000);
const centi = Math.floor((ms % 1000) / 10);
// Skip leading zero units ("0 d"/"00 h"), keep everything after the
// first non-zero unit so lower units never flicker; "0 ms" at boot.
let started = false;
t.textContent = [
[day, 'd'],
[hour, 'h'],
[min, 'm'],
[sec, 's'],
[centi, 'ms']
].reduce((acc, [v, u]) => {
if (!started && v === 0) return acc;
started = true;
return acc + v.toString().padStart(2, '0') + u + ' ';
}, '').trim() || '0 ms';
requestAnimationFrame(loop);
}
// Silent re-sync every 60s: pull the real uptime to correct drift and
// notice restarts. After a restart (or tab throttling), the number snaps
// to the true value — which is exactly what a health page should show.
async function resync() {
try {
const res = await fetch(location.href, { headers: { Accept: 'application/json' } });
if (res.ok) {
const data = await res.json();
base = data.uptime * 1000;
startTs = undefined; // re-anchor so the next frame transitions smoothly
}
} catch {}
setTimeout(resync, 60 * 1000);
}
requestAnimationFrame(loop);
setTimeout(resync, 60 * 1000);
</script>
</body>
</html>`;
}
const server = createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname !== '/health') {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('not found');
return;
}
const uptime = uptimeSeconds();
// THE trick: content negotiation on the Accept header.
// text/html -> live page (browser address bar, curl -H)
// anything else -> JSON (default curl, uptime monitors, CI)
const wantsHtml = (req.headers.accept ?? '').includes('text/html');
if (wantsHtml) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(renderHealthHtml(uptime));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ status: 'ok', uptime }));
});
server.listen(PORT, () => {
console.log(`Health server on http://localhost:${PORT}/health`);
});
@Wxh16144

Copy link
Copy Markdown
Author

One URL, Two Content Types

One URL, two content types — a live HTML page for humans, plain JSON for machines (curl / uptime monitors / CI probes).


EN

  • One route, two responses: Accept: text/html → live page, otherwise JSON.
  • The page gets the initial uptime injected once, then accumulates it locally with requestAnimationFrame — no polling.
  • Every 60s it silently re-fetches the JSON to re-anchor the baseline.
node server.js
Command Result
open http://localhost:3000/health Live HTML page
curl -s http://localhost:3000/health {"status":"ok","uptime":42}

中文

  • 同一个路由两种响应:Accept: text/html → 实时页面,否则 JSON。
  • 初始运行时长服务端注入一次,之后 requestAnimationFrame 本地累加 —— 无轮询。
  • 每 60 秒静默拉一次 JSON 重新锚定基准。
node server.js
命令 结果
open http://localhost:3000/health 实时跳动的 HTML 页面
curl -s http://localhost:3000/health {"status":"ok","uptime":42}

License

MIT

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