koala73/worldmonitor · error · Error

Compact health pending entries must be objects

Error message

Compact health pending entries must be objects

What it means

validateCompactHealthPayload also requires every value inside `pending` to be a non-null, non-array object — one entry per pending seed with its own fields. It throws when any entry is null, an array, or a scalar, because downstream freshness logic reads per-seed properties from each entry and cannot do so safely otherwise.

Solutions

  1. Fix the producer so each pending value is an object with the expected per-seed fields
  2. Update fixtures/mocks to use full entry objects
  3. If the wire format intentionally changed, update validateCompactHealthPayload and downstream readers together in one deploy
  4. Add a schema test on the worker output so entry shape drift is caught before the checker runs

Example fix

// before
{"pending":{"seed:fx": 1719000000000}}
// after
{"pending":{"seed:fx":{"updatedAt":1719000000000,"status":"PENDING"}}}
Defensive patterns

Strategy: type-guard

Validate before calling

const entries = Object.values(payload.pending ?? {});
if (entries.some((e) => e === null || typeof e !== 'object' || Array.isArray(e))) throw new Error('each pending entry must be an object');

Type guard

const pendingEntriesAreObjects = (p) => Object.values(p.pending ?? {}).every((e) => e !== null && typeof e === 'object' && !Array.isArray(e));

Try / catch

try {
  validateCompactHealthPayload(payload);
} catch (e) {
  if (e.message === 'Compact health pending entries must be objects') {
    console.error('A pending seed entry lost its object shape — check the producer');
  } else throw e;
}

Prevention

When it happens

Trigger: A pending map whose values are scalars (e.g. {"seed:foo": 123} timestamps instead of objects) or nulls; manual fixture construction with wrong entry shape; API change flattening entries into plain values.

Common situations: Producer simplifies entries to timestamps/strings to save payload size; test mocks built with placeholder primitives; JSON schema drift between worker and checker versions deployed at different times.

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/3f652c4cd1f7a2aa. Report an issue: GitHub.

Appendix: source

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

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.
//
// Everything else must stay strict even for an on-demand source. `SEED_ERROR`
// means the producer ran and failed; a long `STALE_SEED` means it stopped
// running. Neither is explained by "nobody asked for it yet", and softening

View on GitHub (pinned to 7d06c8633d)