Files
TREK/server/src/app-config/boot-derive.ts
T
Maurice 5cb87a4df5 feat(server): add last-resort error handlers, a draining readiness probe and proxy keep-alive
Three small gaps in how the process behaves at its edges:

- An unhandled rejection or uncaught exception crashed the process with
  the stack on stderr only. index.ts now logs it to trek.log and runs the
  same orderly shutdown a SIGTERM gets, exiting with 1; a second fatal
  error during that shutdown exits at once.
- /api/health/ready kept answering 200 while a SIGTERM was draining the
  process, so an orchestrator sent traffic for another probe period. A
  shutdown now marks the new ReadinessService first (and Nest's own
  shutdown hook marks it too), and the probe answers 503 from then on.
- Node closes an idle keep-alive connection after 5 seconds while reverse
  proxies keep upstream connections for about a minute, which shows up as
  intermittent 502s. The server now keeps them for 65 seconds, configurable
  through HTTP_KEEP_ALIVE_TIMEOUT_MS (owned by the httpConfig token), with
  the headers timeout one second above.
2026-10-08 18:40:37 +02:00

50 lines
2.1 KiB
TypeScript

/**
* Derive functions for the BOOT-STABLE values that Nest injects through a
* registerAs token (src/nest/app-config/tokens.ts) and nothing else reads.
*
* Every environment variable has exactly one owner. A variable derived here is
* owned by its token: `deriveAll()` (and so `readEnv()`) never reads it, which
* tests/unit/app-config/config-ownership.test.ts checks by recording the keys
* each side touches. A value that pre-container code needs, or that tests
* mutate mid-lifetime, belongs in derive.ts instead.
*/
import { parseBool, positiveIntOr } from './parsers';
/**
* Longer than the idle timeout of the common reverse proxies and load
* balancers (60 s), so the proxy closes an idle upstream connection before
* Node does. The other way round the proxy reuses a socket Node just closed
* and answers that request with a 502.
*/
export const DEFAULT_KEEP_ALIVE_TIMEOUT_MS = 65_000;
import type { RawEnv } from './derive';
/** What the pre-init Express layer and the HTTP server freeze when buildApp() runs. */
export function deriveHttpBoot(raw: RawEnv) {
// env.schema.ts accepts 0 as a valid hop count, so `|| 1` would quietly turn
// "trust nothing" into "trust one hop" and let a forged X-Forwarded-For through.
const trustProxyHops = Number.parseInt(raw.TRUST_PROXY ?? '', 10);
return {
trustProxyRaw: raw.TRUST_PROXY,
trustProxy: Number.isFinite(trustProxyHops) ? trustProxyHops : 1,
hstsIncludeSubdomains: parseBool(raw.HSTS_INCLUDE_SUBDOMAINS) === true,
/** How long the HTTP server keeps an idle keep-alive connection open. */
keepAliveTimeoutMs: positiveIntOr(raw.HTTP_KEEP_ALIVE_TIMEOUT_MS, DEFAULT_KEEP_ALIVE_TIMEOUT_MS),
};
}
/** Where the storage registry puts its conditional place-photo backend. */
export function deriveStorage(raw: RawEnv) {
return {
placePhotoDir: raw.TREK_PLACE_PHOTO_DIR,
};
}
/** One entry per token: the namespace it registers under and the function it derives with. */
export const BOOT_DERIVERS = {
http: deriveHttpBoot,
storage: deriveStorage,
} as const;
export type BootNamespace = keyof typeof BOOT_DERIVERS;