Created
August 20, 2026 06:34
-
-
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================================================ | |
| // 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`); | |
| }); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
Accept: text/html→ live page, otherwise JSON.requestAnimationFrame— no polling.open http://localhost:3000/healthcurl -s http://localhost:3000/health{"status":"ok","uptime":42}中文
Accept: text/html→ 实时页面,否则 JSON。requestAnimationFrame本地累加 —— 无轮询。open http://localhost:3000/healthcurl -s http://localhost:3000/health{"status":"ok","uptime":42}License
MIT