Skip to content

Add health check endpoint, error pages, and environment validation - #271

Merged
2witstudios merged 6 commits into
masterfrom
claude/find-missing-basics-bFeRI
Jan 29, 2026
Merged

2witstudios merged 6 commits into
masterfrom
claude/find-missing-basics-bFeRI

Conversation

@2witstudios

@2witstudios 2witstudios commented Jan 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR adds critical infrastructure improvements for system reliability and observability, including a health check endpoint for load balancers, global error handling pages, request ID propagation for distributed tracing, and comprehensive environment variable validation.

Key Changes

Health Check Endpoint (/api/health)

  • New GET /api/health endpoint that returns system health status with HTTP 200 (healthy) or 503 (degraded)
  • Includes database connectivity checks, memory usage metrics, service version, and timestamps
  • Implements proper cache-control headers to prevent caching
  • Comprehensive test coverage with 145 lines of test cases covering healthy states, database failures, and caching behavior

Error Handling Pages

  • Global Error Boundary (error.tsx): Catches unhandled errors with user-friendly UI, error details, and development stack traces
  • 404 Not Found Page (not-found.tsx): Custom page for missing routes with navigation options and GitHub issue reporting link
  • Both pages use consistent Card-based UI with appropriate icons and action buttons

Request ID Propagation

  • New request-id utility module for distributed tracing support
  • getOrCreateRequestId(): Extracts incoming X-Request-Id header or generates new CUID2 ID
  • isValidRequestId(): Validates IDs to prevent header injection attacks (alphanumeric, hyphens, underscores, max 128 chars)
  • Updated monitoring middleware to use the new utility, preserving upstream request IDs for tracing
  • 132 lines of comprehensive tests covering validation, generation, and edge cases

Environment Variable Validation

  • New env-validation module with Zod schema for server-side configuration
  • Validates required vars: DATABASE_URL, JWT_SECRET, JWT_ISSUER, JWT_AUDIENCE, CSRF_SECRET, ENCRYPTION_KEY
  • Supports optional vars with sensible defaults (NODE_ENV, LOG_LEVEL, OAuth, AI keys, monitoring, Stripe)
  • Three validation functions: validateEnv() (throws), getEnvErrors() (returns array), isEnvValid() (returns boolean)
  • Caching mechanism via getValidatedEnv() to validate only once
  • 204 lines of tests covering schema validation, error handling, and optional variables

Fetch Utility Enhancement

  • New fetchWithTimeout utility for external API calls with timeout protection
  • Custom TimeoutError class for distinguishing timeout errors
  • Predefined timeout constants: SHORT (5s), MEDIUM (15s), DEFAULT (30s), LONG (60s), EXTENDED (120s)
  • 159 lines of tests covering successful responses, custom options, error propagation, and timeout scenarios

Implementation Details

  • Security: Request ID validation prevents header injection; environment validation ensures secure defaults
  • Observability: Health checks enable proactive monitoring; request IDs enable distributed tracing across services
  • Reliability: Timeout handling prevents resource exhaustion; error boundaries prevent white-screen crashes
  • Testing: All new code includes comprehensive test coverage with clear test descriptions following BDD patterns
  • Backward Compatibility: Monitoring middleware changes are non-breaking; existing request ID generation still works

https://claude.ai/code/session_01YKdWhqHpkjMH9Eobi9NpvH

Summary by CodeRabbit

  • New Features

    • Health check endpoint reporting status, timestamp, version, memory, and DB connectivity.
    • Global error page and a user-friendly 404 page with navigation actions.
    • Timeout-enabled fetch utility and request-id generation/validation utilities.
    • Server-side environment validation with runtime helpers.
  • Improvements

    • Preserve incoming request IDs for distributed tracing.
    • Cached/validated env retrieval to avoid repeated parsing.
  • Tests

    • Comprehensive test suites for health, request-id, env validation, fetch timeouts, and adjusted DB cleanup ordering.

✏️ Tip: You can customize this high-level summary in your review settings.

Add foundational production features that were missing:

- Environment validation with Zod schema at startup
  - Validates required env vars (DATABASE_URL, JWT_SECRET, etc.)
  - Prevents runtime failures from missing configuration
  - packages/lib/src/config/env-validation.ts

- Health check endpoint for load balancers and orchestration
  - GET /api/health with database connectivity check
  - Memory usage metrics and service info
  - Proper Cache-Control headers
  - apps/web/src/app/api/health/route.ts

- Global error pages for better UX
  - error.tsx with retry and navigation options
  - not-found.tsx with branded 404 page
  - apps/web/src/app/error.tsx, not-found.tsx

- Request ID middleware for distributed tracing
  - Preserves incoming X-Request-Id for tracing
  - Generates new ID if not present
  - apps/web/src/lib/request-id/request-id.ts

- Fetch with timeout utility for external API calls
  - Prevents hanging requests from exhausting resources
  - Predefined timeout constants (SHORT, MEDIUM, LONG)
  - TimeoutError class for proper error handling
  - packages/lib/src/utils/fetch-with-timeout.ts

All features include comprehensive TDD test coverage.

https://claude.ai/code/session_01YKdWhqHpkjMH9Eobi9NpvH
@coderabbitai

coderabbitai Bot commented Jan 29, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a health check API route and tests, request-id utilities and middleware integration, global Error and NotFound UI pages, environment validation and timeout-aware fetch utilities with tests, and adjusts several tests' DB cleanup ordering and imports.

Changes

Cohort / File(s) Summary
Health Check Endpoint
apps/web/src/app/api/health/route.ts, apps/web/src/app/api/health/__tests__/route.test.ts
New GET /api/health handler performing a lightweight DB check, capturing memory/timestamp/version, logging outcomes, and returning 200 (healthy) or 503 (degraded) with Cache-Control: no-store, no-cache, must-revalidate. Tests simulate healthy, DB connection failure, and query timeout scenarios.
Request ID Utilities & Middleware
apps/web/src/lib/request-id/request-id.ts, apps/web/src/lib/request-id/__tests__/request-id.test.ts, apps/web/src/middleware/monitoring.ts
Adds REQUEST_ID_HEADER, isValidRequestId, getOrCreateRequestId, and createRequestId. Middleware now preserves incoming request IDs and emits the header constant. Tests validate header acceptance/rejection, generation, uniqueness, and edge cases.
UI: Error & NotFound Pages
apps/web/src/app/error.tsx, apps/web/src/app/not-found.tsx
Adds a client-side Error boundary component (retry and navigate home, optional dev stack) and a Not Found page with navigation actions and an issue-report link.
Environment Validation
packages/lib/src/config/env-validation.ts, packages/lib/src/config/__tests__/env-validation.test.ts
Adds a Zod-based serverEnvSchema, ServerEnv type, validateEnv, getEnvErrors, isEnvValid, and getValidatedEnv (cached). Enforces required vars with differing rules for test vs non-test environments; comprehensive tests added.
Fetch-with-Timeout Utility
packages/lib/src/utils/fetch-with-timeout.ts, packages/lib/src/utils/__tests__/fetch-with-timeout.test.ts
Adds DEFAULT_TIMEOUT_MS, TIMEOUTS presets, TimeoutError, and fetchWithTimeout that uses AbortController and preserves caller signals. Tests cover timeout behavior, signal forwarding, option passthrough, and error types.
Tests: DB cleanup ordering & imports
packages/lib/src/__tests__/file-processor.test.ts, packages/lib/src/__tests__/permissions-cached.test.ts, packages/lib/src/__tests__/permissions.test.ts, packages/lib/src/permissions/__tests__/permission-mutations.test.ts
Expands imports from @pagespace/db (pagePermissions, driveMembers, pages, drives) and changes test cleanup to explicit foreign-key-respecting delete order to reduce deadlock risk.
Misc Tests Additions
apps/web/src/lib/request-id/__tests__/request-id.test.ts, packages/lib/src/utils/__tests__/fetch-with-timeout.test.ts, packages/lib/src/config/__tests__/env-validation.test.ts
Adds comprehensive tests for request-id, fetch-with-timeout, and env validation utilities covering success, failure, and edge cases.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant HealthRoute as Health Route
  participant DB as Database
  participant Logger
  Client->>HealthRoute: GET /api/health
  HealthRoute->>DB: lightweight query (SELECT 1)
  DB-->>HealthRoute: success / error / timeout
  HealthRoute->>Logger: info / warn / error (based on DB result)
  HealthRoute->>HealthRoute: gather process.memoryUsage(), timestamp, version
  HealthRoute-->>Client: 200 (healthy) or 503 (degraded) with JSON + Cache-Control:no-store, no-cache, must-revalidate
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐇 I hopped through checks both small and grand,
I poked the DB with a careful hand,
IDs snug and timeouts kept in line,
Errors caught kindly — the burrow’s fine,
A tiny hop for code, a crunchy carrot sign. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the three major components added: health check endpoint, error pages, and environment validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@apps/web/src/app/api/health/route.ts`:
- Around line 60-66: The route currently constructs responses with new
Response(JSON.stringify(response), { status: statusCode, headers: {
'Content-Type': 'application/json', ... } }); — replace these with
Response.json(response, { status: statusCode, headers: { 'Cache-Control':
'no-store, no-cache, must-revalidate' } }) so Content-Type is set automatically;
do the same replacement for the error response that builds JSON manually (the
block using JSON.stringify(errorResponse) / statusCode for errors) and remove
the explicit 'Content-Type' header in both places.

In `@apps/web/src/app/error.tsx`:
- Around line 31-34: Update the misleading copy inside CardDescription (near
CardTitle "Something went wrong" in error.tsx) so it no longer claims the team
was notified; either replace the sentence with a neutral message like "An
unexpected error occurred. Please try again or contact support." or wire actual
telemetry by invoking your telemetry/logging function (e.g., call trackException
or sendErrorReport from your analytics/logger in the error boundary that renders
this Card) and then keep the original "team has been notified" text only if that
telemetry call is added and awaited/handled.

In `@apps/web/src/app/not-found.tsx`:
- Around line 12-43: This file is a Server Component but uses client-only
features (onClick handler and history.back()) in NotFoundPage; add the 'use
client' directive as the very first line of the file to opt into a Client
Component so the Button onClick and history.back() will run in the browser, then
keep the existing onClick={() => history.back()} (or optionally replace with
window.history.back() or next/router's router.back() inside the same component)
to ensure the back action executes on the client.

In `@apps/web/src/lib/request-id/request-id.ts`:
- Around line 10-21: The isValidRequestId function is too permissive; update it
to only accept CUID2 IDs by replacing the generic alphanumeric/underscore/hyphen
check with a strict CUID2 validation. Locate isValidRequestId and either call
the project’s CUID2 validator/util (preferred) or replace the regex with the
canonical CUID2 pattern from the spec (lowercase alphanumeric structure and
exact length constraints used across PageSpace) so only valid CUID2 strings
pass.

In `@packages/lib/src/config/__tests__/env-validation.test.ts`:
- Around line 17-27: The test suite leaks host environment into tests causing
flakiness; update the beforeEach in the 'env-validation' describe block so it
does not restore the full originalEnv but instead resets process.env to a
minimal, deterministic set (or an empty object) and explicitly set any required
vars used by validateEnv()/getEnvErrors(); keep afterEach restoring originalEnv
to avoid side effects. Locate the beforeEach/afterEach block and change
process.env = { ...originalEnv } to something like process.env = { NODE_ENV:
'test', MY_REQUIRED_VAR: 'value' } or {} and explicitly clear optional URL/env
keys that can break validation so validateEnv() and getEnvErrors() run
deterministically.

In `@packages/lib/src/utils/fetch-with-timeout.ts`:
- Around line 43-73: The fetchWithTimeout function currently overwrites the
caller's AbortSignal and treats all AbortError as timeouts; change it to compose
the caller signal with the internal AbortController by adding an event listener
to the caller's signal (using AbortSignal.addEventListener) that calls
controller.abort() and set a boolean flag (e.g., timedOut) in the timeout
callback to indicate the internal timer fired, ensure you remove the external
listener and clear the timeout in the finally block, and only map an AbortError
to a TimeoutError when that timedOut flag is true (otherwise rethrow the
original abort error).
🧹 Nitpick comments (1)
apps/web/src/app/api/health/__tests__/route.test.ts (1)

33-96: Optional: extract small helpers to reduce repetition.

The healthy-path tests repeat request creation and DB setup; a tiny helper improves readability and keeps future additions consistent.

♻️ Suggested refactor
+const createHealthRequest = () =>
+  new Request('https://example.com/api/health', { method: 'GET' });
+
+const mockDbHealthy = () => {
+  mockExecute.mockResolvedValue([{ '1': 1 }]);
+};
+
 describe('GET /api/health', () => {
   beforeEach(() => {
     vi.clearAllMocks();
   });
 
   describe('healthy system', () => {
     it('given database is connected, should return healthy status', async () => {
-      mockExecute.mockResolvedValue([{ '1': 1 }]);
+      mockDbHealthy();
 
-      const request = new Request('https://example.com/api/health', {
-        method: 'GET',
-      });
+      const request = createHealthRequest();

Comment thread apps/web/src/app/api/health/route.ts Outdated
Comment thread apps/web/src/app/error.tsx
Comment thread apps/web/src/app/not-found.tsx
Comment thread apps/web/src/lib/request-id/request-id.ts Outdated
Comment thread packages/lib/src/config/__tests__/env-validation.test.ts
Comment thread packages/lib/src/utils/fetch-with-timeout.ts
- Make CSRF_SECRET and ENCRYPTION_KEY optional in test environment
- Use Zod superRefine for conditional validation at parse time
- Add tests for test environment validation behavior
- Fix unused request parameter lint error in health route

https://claude.ai/code/session_01YKdWhqHpkjMH9Eobi9NpvH
- Remove docblocks that reiterate what code/names already express
- Remove redundant inline comments (e.g., regex explanation)
- Keep minimal docblocks for public API functions that add value
- Clean up test file headers (describe blocks are self-explanatory)

https://claude.ai/code/session_01YKdWhqHpkjMH9Eobi9NpvH
- Use Response.json() instead of new Response(JSON.stringify()) in health route
- Fix misleading "team notified" copy in error.tsx
- Add 'use client' to not-found.tsx for onClick/history.back()
- Make isValidRequestId CUID2-only using official isCuid validator
- Fix env-validation test isolation (reset to minimal deterministic env)
- Fix fetchWithTimeout to compose AbortSignals properly (caller vs timeout)

https://claude.ai/code/session_01YKdWhqHpkjMH9Eobi9NpvH
Delete tables in foreign key order (children first) to avoid
deadlock from cascade contention when cleaning up test data:
1. pagePermissions
2. pages
3. driveMembers
4. drives
5. users

This prevents the PostgreSQL deadlock that occurred in CI when
cascading deletes from the users table tried to acquire locks
on multiple tables simultaneously.

https://claude.ai/code/session_01YKdWhqHpkjMH9Eobi9NpvH

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/app/error.tsx`:
- Around line 37-47: Replace direct rendering of error.message (and optionally
error.digest) with an environment-gated display: in the component that renders
CardContent, change the JSX to show a generic user-facing message like "An
unexpected error occurred" when process.env.NODE_ENV !== 'development', and only
render the raw error.message (and detailed digest) when process.env.NODE_ENV ===
'development' so detailed error text is available for dev-only views; update
both occurrences where error.message/error.digest are used to follow this
pattern.
🧹 Nitpick comments (3)
packages/lib/src/utils/__tests__/fetch-with-timeout.test.ts (2)

100-112: Consider adding assertion for actual signal composition.

This test verifies that passing a caller signal doesn't break the function, but it doesn't verify that the composition actually works—e.g., that aborting the caller's signal mid-request triggers the abort. Consider adding a test that aborts callerController while fetch is pending and verifies the original AbortError propagates.


114-134: Consider verifying the default timeout was actually applied.

These tests confirm that zero/negative timeouts don't cause errors, but they don't assert that DEFAULT_TIMEOUT_MS was used. You could spy on setTimeout to verify it was called with 30000.

💡 Example assertion
it('given zero timeout, should use default timeout', async () => {
  const mockResponse = new Response('ok', { status: 200 });
  global.fetch = vi.fn().mockResolvedValue(mockResponse);
  const setTimeoutSpy = vi.spyOn(global, 'setTimeout');

  await fetchWithTimeout('https://api.example.com/data', {
    timeout: 0,
  });

  expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), DEFAULT_TIMEOUT_MS);
});
apps/web/src/app/error.tsx (1)

50-63: Use router-based navigation to preserve client state and keep navigation in-app.

Replace window.location.href = '/dashboard' with router.push('/dashboard') from Next.js's useRouter hook. Error boundaries are client components, so hooks are fully supported.

♻️ Suggested refactor
import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
@@
 export default function ErrorPage({ error, reset }: ErrorPageProps) {
+  const router = useRouter();
   useEffect(() => {
     console.error('Global error boundary caught:', error);
   }, [error]);
@@
             <Button
               variant="outline"
-              onClick={() => (window.location.href = '/dashboard')}
+              onClick={() => router.push('/dashboard')}
               className="w-full"
             >

Comment thread apps/web/src/app/error.tsx Outdated
Comment on lines +37 to +47
<CardContent className="space-y-4">
<div className="bg-muted p-3 rounded-md text-sm">
<div className="font-medium mb-1">Error Details:</div>
<div className="text-muted-foreground break-words">
{error.message || 'An unexpected error occurred'}
</div>
{error.digest && (
<div className="text-xs text-muted-foreground mt-2">
Error ID: {error.digest}
</div>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid leaking raw error messages to end users in production.

error.message can expose sensitive details; prefer a generic message outside dev and keep detailed info for dev-only views.

🔒 Suggested fix: gate detailed messages to development
 export default function ErrorPage({ error, reset }: ErrorPageProps) {
   useEffect(() => {
     console.error('Global error boundary caught:', error);
   }, [error]);
+  const IS_DEV = process.env.NODE_ENV === 'development';

   return (
@@
             <div className="text-muted-foreground break-words">
-              {error.message || 'An unexpected error occurred'}
+              {IS_DEV && error.message
+                ? error.message
+                : 'An unexpected error occurred'}
             </div>
@@
-          {process.env.NODE_ENV === 'development' && error.stack && (
+          {IS_DEV && error.stack && (

Also applies to: 66-76

🤖 Prompt for AI Agents
In `@apps/web/src/app/error.tsx` around lines 37 - 47, Replace direct rendering of
error.message (and optionally error.digest) with an environment-gated display:
in the component that renders CardContent, change the JSX to show a generic
user-facing message like "An unexpected error occurred" when
process.env.NODE_ENV !== 'development', and only render the raw error.message
(and detailed digest) when process.env.NODE_ENV === 'development' so detailed
error text is available for dev-only views; update both occurrences where
error.message/error.digest are used to follow this pattern.

Only show error.message in development to prevent information
disclosure. The error.digest (Error ID) is still shown in production
for support reference.

Addresses CodeRabbit review comment on PR #271.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios
2witstudios merged commit 6f71585 into master Jan 29, 2026
10 checks passed
@2witstudios
2witstudios deleted the claude/find-missing-basics-bFeRI branch January 29, 2026 20:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants