Repository navigation
Stop REST /health from leaking error text or taking every /api route down #405
Description
Summary
The REST /health route runs a database query, a Redis PING and a Supabase call on every request. It skips the app rate limiter, and its Traefik router has no rate limit. The rest-api Docker healthcheck calls this same route. Traefik stops routing to a container whose Docker health is unhealthy. So a /health flood, or a Redis stall of about 50 s, can mark both replicas unhealthy and take every /api route down. The REST routes keep working without Redis, because the rate limiter fails open.
When a check fails, the response also carries the raw driver error text.
- Severity: Medium (availability), Low (information leak)
- Area:
apps/hocuspocus.serverhealth routes,docker-compose.prod.yml - Source: security review of 2026-10-06, findings M12 and L16
Where
- Traefik router
rest-api-healthhassecurity-headers@fileonly:docker-compose.prod.yml:228-233. - The Traefik load-balancer probe already uses liveness (
/health/live):docker-compose.prod.yml:234-241. This part is correct and stays. - The Docker healthcheck calls
/health:docker-compose.prod.yml:242-256. Its comment (:242-244) says the deploy waits on this status, so it must check a dependency, not liveness. - The deploy waits on that status before it stops the old replicas:
.github/workflows/prod.docs.plus.yml:502-518. - The app limiter skips
/healthand/health/*:apps/hocuspocus.server/src/middleware/index.ts:248-251. - The limiter fails open when Redis is down:
apps/hocuspocus.server/src/middleware/index.ts:71-90. - Checks:
apps/hocuspocus.server/src/api/services/health.service.ts:39-124, combined incheckAllServices(:126-153). Redis counts as critical at:142-144. - Raw error text:
health.service.ts:51,:86and:121seterror: errorMessage(error). - Every
/health/*sub-route also runs its check uncached:apps/hocuspocus.server/src/api/routers/health.router.ts:15-17. - Consumers read the HTTP status only, never the body:
- The admin dashboard:
apps/admin-dashboard/src/services/api.ts:201-214(checkApiHealth). - The blackbox probe:
scripts/observability/prometheus/prometheus.yml:67-68. - The deploy smoke:
.github/workflows/prod.docs.plus.yml:744.
- The admin dashboard:
Fix plan
- Cache each dependency check for 5 s in
health.service.ts.- Wrap
checkDatabaseHealth,checkRedisHealthandcheckSupabaseHealthin one small memo. It keeps the last promise and its start time, so concurrent callers share one check. - This covers
/healthand the/health/database,/health/redisand/health/supabasesub-routes. - Export a
clearHealthCache()and call it inbeforeEachoftests/integration/health.test.ts. Those tests swap the mocks per test.
- Wrap
- Drop the error text from responses. In the three
catchblocks, log the error withrestApiLogger.warn({ err, check })and return{ status: 'unhealthy', lastCheck }with noerrorfield.- Remove
errorfrom theHealthCheckResultschema inapps/hocuspocus.server/src/modules/openapi/domain/components.ts:124. - Remove
error?fromHealthCheckResultinsrc/types/health.types.ts:6. Nothing else sets it.
- Remove
- Point the Docker healthcheck at
/health/databaseindocker-compose.prod.yml:251anddocker-compose.dev.yml:132.- The deploy still waits for a working database, which REST cannot serve without.
- A Redis stall no longer marks the replicas unhealthy.
- Update the comment at
docker-compose.prod.yml:242-244to say why Redis is left out.
- Update
apps/hocuspocus.server/API.md§Health. Say the checks are cached for 5 s and failures carry no error text.
Layer: the fix stays in health.service.ts and the compose healthcheck. Do not change the Traefik /health/live probe. Do not change what /health reports as degraded; the dashboard and the blackbox alert need Redis in it. This is the rest-api container only. The Next Health rule in CLAUDE.md §Next product APIs covers the webapp probe, not this one. Still get maintainer sign-off on the compose change, because its comment records a deliberate choice.
Out of scope
- A Traefik rate limit on
rest-api-health. With the cache, a request costs no dependency call. Add one only if a flood still shows up in metrics. - The
/health/pushbody. Move the email and push gateway reports behind admin auth #433 moves the push and email gateway reports behind admin auth. - Redis
metadatain/health/redis. It holds connection state and queue lengths, not hosts or ports.
Acceptance criteria
- With Redis stopped for 60 s, the
rest-apicontainers stayhealthy, and/apiroutes still answer. - With the database stopped, the
rest-apicontainers turnunhealthy, as before. - No
/healthor/health/*response contains a driver message, host name or port. - 1000 rapid
GET /healthcalls cause at most one database query per 5 s window. - The admin dashboard System page and the blackbox probe still report status.
Verify
cd apps/hocuspocus.server && bun test tests/integration/health.test.ts. Update the tests that assertdata.error(:88-89,:103,:145-146,:160,:186) to assert that noerrorfield exists.- Add one test: two calls inside 5 s call
$queryRawonce. - Dev stack:
docker compose -f docker-compose.dev.yml stop redis, wait 70 s, thendocker inspect -f '{{.State.Health.Status}}' <rest-api container>readshealthy. Start Redis again.
Related
- Move the email and push gateway reports behind admin auth #433 (email and push gateway reports are public)
- Keep raw database error text out of production 500 responses #413 (raw error text in 500 responses, fixed)
Generated by Claude Code
Activity
[-][Security] Public REST /health has no rate limit, returns raw dependency errors, and can take every /api route down[/-][+]Stop REST /health from leaking error text or taking every /api route down[/+]fix(hocuspocus): cache REST health checks and keep error text out of …
docs(openapi): match the spec to today's admin, email, media and heal…
docs: update the API and self-hosting guides for the 9 Oct backend fixes
HMarzban commentedon Oct 9, 2026
Fixed and live.
1ad6f5821bbdaae66c(production run 37976785981, green).GET /health/databaseanswers 200{status, lastCheck}with no error text. Each REST health check is cached for 5 s.