koala73/worldmonitor · error · Error

Compact health pending must be an object

Error message

Compact health pending must be an object

What it means

Within validateCompactHealthPayload, if the compact health object carries a `pending` key it must be a non-null, non-array object whose values are entry objects. This check throws when `pending` is present but is null, an array, or a primitive, preventing downstream Object.values iteration from failing or silently mis-measuring pending seeds.

Solutions

  1. Fix the health endpoint/serializer to omit `pending` when empty or emit it as a keyed object
  2. If pending is now a list by design, update validateCompactHealthPayload and its consumers to the new shape
  3. Correct test fixtures/mocks to use an object map for pending
  4. Guard at the producer side: only include `pending` when it is a populated object

Example fix

// before
{"status":"HEALTHY","pending":null}
// after
{"status":"HEALTHY"}
Defensive patterns

Strategy: type-guard

Validate before calling

if ('pending' in payload && (payload.pending === null || typeof payload.pending !== 'object' || Array.isArray(payload.pending))) throw new Error('pending must be a keyed object when present');

Type guard

const hasValidPending = (p) => !('pending' in p) || (p.pending !== null && typeof p.pending === 'object' && !Array.isArray(p.pending));

Try / catch

try {
  validateCompactHealthPayload(payload);
} catch (e) {
  if (e.message === 'Compact health pending must be an object') {
    console.error('pending shape drifted from the compact health contract');
  } else throw e;
}

Prevention

When it happens

Trigger: Health endpoint emitting `pending: null` (serialization of undefined), `pending: []` (array instead of keyed map), or `pending: "none"`; caller constructing the payload manually with a wrong-shaped pending field.

Common situations: API change where pending became a list of objects; a stub/mock fixture using an array; JSON produced by code that sets pending to null when empty instead of omitting the key.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/8f90266a8e72cb14. Report an issue: GitHub.

Appendix: source

Thrown at scripts/check-seed-freshness.mjs:20

import { readFileSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';

const DEFAULT_HEALTH_URL = 'https://api.worldmonitor.app/api/health?compact=1';
const BASELINE_URL = new URL('./seed-freshness-baseline.json', import.meta.url);
// api/health.js only serves a cached verdict for 60 seconds. Allow its maximum
// 20-second request timeout too, so a valid snapshot cannot be rejected solely
// because the response arrived at the end of the monitor's fetch window.
export const MAX_HEALTH_OBSERVATION_AGE_MS = 80 * 1000;

export function validateCompactHealthPayload(payload) {
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
    throw new Error('Compact health payload must be an object');
  }
  if (Object.hasOwn(payload, 'pending')) {
    if (!payload.pending || typeof payload.pending !== 'object' || Array.isArray(payload.pending)) {
      throw new Error('Compact health pending must be an object');
    }
    for (const entry of Object.values(payload.pending)) {
      if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
        throw new Error('Compact health pending entries must be objects');
      }
    }
  }
  // Compact health omits `problems` entirely when every check is healthy.
  if (payload.problems == null && payload.status === 'HEALTHY') return payload;
  if (!payload.problems || typeof payload.problems !== 'object' || Array.isArray(payload.problems)) {
    throw new Error('Compact health payload must contain a problems object');
  }
  return payload;
}

// The ONLY states being on-demand actually explains: nothing has requested the
// key yet, or the producer has not run for the first time. Absence is expected
// for an RPC-populated cache or a deployment-order bridge, so it must not page.

View on GitHub (pinned to 7d06c8633d)