{"record":{"id":"48692978789b35ce","repo":"koala73/worldmonitor","slug":"compact-health-payload-must-be-an-object","errorCode":null,"errorMessage":"Compact health payload must be an object","messagePattern":"Compact health payload must be an object","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/check-seed-freshness.mjs","lineNumber":16,"sourceCode":"#!/usr/bin/env node\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { parseArgs } from 'node:util';\n\nconst DEFAULT_HEALTH_URL = 'https://api.worldmonitor.app/api/health?compact=1';\nconst BASELINE_URL = new URL('./seed-freshness-baseline.json', import.meta.url);\n// api/health.js only serves a cached verdict for 60 seconds. Allow its maximum\n// 20-second request timeout too, so a valid snapshot cannot be rejected solely\n// because the response arrived at the end of the monitor's fetch window.\nexport const MAX_HEALTH_OBSERVATION_AGE_MS = 80 * 1000;\n\nexport function validateCompactHealthPayload(payload) {\n  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\n    throw new Error('Compact health payload must be an object');\n  }\n  if (Object.hasOwn(payload, 'pending')) {\n    if (!payload.pending || typeof payload.pending !== 'object' || Array.isArray(payload.pending)) {\n      throw new Error('Compact health pending must be an object');\n    }\n    for (const entry of Object.values(payload.pending)) {\n      if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n        throw new Error('Compact health pending entries must be objects');\n      }\n    }\n  }\n  // Compact health omits `problems` entirely when every check is healthy.\n  if (payload.problems == null && payload.status === 'HEALTHY') return payload;\n  if (!payload.problems || typeof payload.problems !== 'object' || Array.isArray(payload.problems)) {\n    throw new Error('Compact health payload must contain a problems object');\n  }\n  return payload;\n}","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/check-seed-freshness.mjs#L1-L34","documentation":"check-seed-freshness validates the compact health payload fetched from the monitor before inspecting seed freshness. validateCompactHealthPayload requires the payload to be a non-null, non-array object; anything else (null, undefined, a JSON array, a string/number) is rejected. This ensures downstream Object.values/Object.hasOwn checks operate on a real object shape.","triggerScenarios":"Calling validateCompactHealthPayload with the result of a fetch that returned null/undefined, a JSON array instead of an object, or a string (e.g. an HTML error page parsed loosely or raw text passed through).","commonSituations":"Health endpoint returned an error page or empty body that got JSON-parsed into something unexpected; API version change reshaped the response into an array; caller passed the raw response text instead of the parsed JSON object.","solutions":["Log/inspect the fetched payload and fix the fetch/parse step so it yields the compact health object","Ensure the caller awaits response.json() and passes that parsed value, not raw text","Check the health endpoint is returning the compact payload (correct URL/version) rather than an error body","Wrap the call in a try/catch that reports the invalid payload type for diagnosis"],"exampleFix":"// before\nvalidateCompactHealthPayload(await res.text());\n// after\nconst payload = await res.json();\nif (payload && !Array.isArray(payload)) validateCompactHealthPayload(payload);","handlingStrategy":"type-guard","validationCode":"const payload = await res.json();\nif (payload === null || typeof payload !== 'object' || Array.isArray(payload)) throw new Error('health endpoint did not return an object');","typeGuard":"const isCompactHealthPayload = (p) => p !== null && typeof p === 'object' && !Array.isArray(p);","tryCatchPattern":"try {\n  validateCompactHealthPayload(payload);\n} catch (e) {\n  if (e.message === 'Compact health payload must be an object') {\n    console.error('Non-object payload from health endpoint; check fetch/parse and endpoint version');\n  } else throw e;\n}","preventionTips":["Always parse the response with res.json() before validating","Check the health endpoint URL/version returns the compact payload","Return an error object (never null/array) from fetch wrappers","Add a contract test asserting the endpoint's top-level shape"],"tags":["validation","schema","health","json"],"backgroundTag":"schema-validation-failed","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}