Lifecycle & health
Daloy ships the lifecycle & health slice of the secure-by-default initiative: connection-draining shutdown, crash-on-unhandled-rejection, and first-class healthcheck() / readinesscheck() primitives. All three default-on the safe behaviour and let you opt out per-feature or globally with app({ secureDefaults: false }).
1. Connection-draining shutdown
app.shutdown(timeoutMs, reason?) (alias app.close()) flips a drain flag synchronously. Every subsequent request returns a structured 503 application/problem+json with retry-after: 5 and connection: close. Any in-flight response that finishes during the drain window also gains connection: close so HTTP/1.1 load balancers stop reusing the keep-alive socket.
On the Node adapter, serve(app) registers an idle-connection close hook that calls server.closeIdleConnections() the moment draining begins — keep-alive sockets without an in-flight request are killed immediately, without affecting sockets that are still serving a request. Custom adapters can register the same hook via app._registerIdleConnectionCloseHook(hook).
2. Crash on unhandled rejection in production
The new app({ crashOnUnhandledRejection }) option installs process-wide listeners for unhandledRejection and uncaughtException that log fatal through the pluggable logger and call process.exit(1). The framework deliberately avoids the "swallow and keep running" anti-pattern — a crashed process is easier to reason about than a zombie one. Defaults:
- Omitted: install in production (
env: "production"orNODE_ENV === "production"), skip elsewhere. true: install even in development (useful for staging / CI).false: never install, even in production.
A process-wide latch ensures the listeners are installed exactly once even when multiple App instances boot in the same process. No-op on runtimes without process.on (Cloudflare Workers, Vercel Edge, Fastly Compute).
3. app.healthcheck() and app.readinesscheck()
Opt-in route registration with sensible defaults: paths /healthz and /readyz, per-IP fixed-window rate limit (60 requests / 60 s, in-memory), optional bearer-token auth compared via timingSafeEqual. Readiness returns 503 with retry-after: 5 while draining or while any plugin is still pending in register().
Pass rateLimit: false to disable the per-IP cap entirely (sidecar-only probes that arrive directly from the orchestrator). The limiter deliberately does not honour X-Forwarded-For: probes typically arrive from a sidecar so spoofing the header should not bypass the cap.
Opt-out
Disable the whole slice with new App({ secureDefaults: false }), or just the crash handlers with new App({ crashOnUnhandledRejection: false }). Health and readiness routes are opt-in — no auto-registration happens, the framework only flips behaviour when you call app.healthcheck() / app.readinesscheck() explicitly.