{"record":{"id":"90bf6997a4698cad","repo":"koala73/worldmonitor","slug":"portwatch-cache-read-returned-invalid-result","errorCode":null,"errorMessage":"PortWatch cache read returned invalid result","messagePattern":"PortWatch cache read returned invalid result","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/seed-portwatch-port-activity.mjs","lineNumber":1042,"sourceCode":"\nconst CORRUPT_COUNTRY_CACHE = Symbol('corrupt country cache');\n\n// MGET-style batch read via the Upstash REST /pipeline endpoint. Returns an\n// array aligned with `keys` where each element is either the parsed JSON\n// payload, explicit miss, or confirmed corrupt value. Transport/envelope errors\n// remain fatal: only a validated upstream replacement may overwrite corruption.\n// Primes the per-country cache lookup in one round-trip instead of 174 GETs.\nasync function redisMgetJson(keys) {\n  if (keys.length === 0) return [];\n  const commands = keys.map((k) => ['GET', k]);\n  const results = await redisPipeline(commands);\n  if (!Array.isArray(results) || results.length !== keys.length) {\n    throw new Error('PortWatch cache read returned incomplete results');\n  }\n  return results.map((r) => {\n    if (r?.error || !Object.hasOwn(r ?? {}, 'result')) throw new Error('PortWatch cache read failed');\n    if (r.result === null) return null;\n    if (typeof r.result !== 'string') throw new Error('PortWatch cache read returned invalid result');\n    try {\n      const payload = JSON.parse(r.result);\n      return payload && typeof payload === 'object' && !Array.isArray(payload)\n        ? payload : CORRUPT_COUNTRY_CACHE;\n    } catch {\n      return CORRUPT_COUNTRY_CACHE;\n    }\n  });\n}\n\n// fetchAll() — pure data collection, no Redis writes.\n// Returns { countries: string[], countryData: Map<iso2, payload>, fetchedAt: string }.\n//\n// `progress` (optional) is mutated in-place so a SIGTERM handler in main()\n// can report which batch / country we died on.\n//\n// Orders cold-fetches by the oldest ATTEMPT, using the last successful cache\n// write as the legacy fallback. This is the durable rotation cursor:","sourceCodeStart":1024,"sourceCodeEnd":1060,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/scripts/seed-portwatch-port-activity.mjs#L1024-L1060","documentation":"This seed script reads a batch of Redis cache entries for PortWatch country data and validates each raw result. Every entry in the pipeline response must be a string (the cached JSON text); if a result object is neither null nor a string, the script throws 'PortWatch cache read returned invalid result', meaning the stored cache value has an unexpected type (e.g. a number, object, or binary value was written where a JSON string was expected).","triggerScenarios":"The Redis pipeline in the cache read returns a result entry that is non-null and not a string: typically a key was written with a non-string type (SET with JSON-serialized object instead of string, HSET leftover, or a client that auto-parsed values), or a serializer/decoder option (e.g. Redis JSON auto-deserialization) was enabled on the read client but not accounted for.","commonSituations":"A different seed run or worker wrote country cache keys with a different encoding; switching Redis client libraries where one returns buffers/objects; enabling a Redis JSON module or client 'json' mode so results come back as objects; stale keys written by an older script version.","solutions":["Inspect the offending key's Redis type with TYPE <key> and DELETE/rewrite keys that are not strings","Write cache values strictly with SET/SETNX as JSON.stringify(payload) strings","Ensure the Redis client used for reading does not auto-decode/auto-parse values (disable json/buffer transformations)","Purge the PortWatch cache keys (KEY_PREFIX*) and re-run the seed to rewrite entries as strings","Pin the same Redis client library/options between writer and reader"],"exampleFix":"// before\nawait redis.set(key, payload);\n// after\nawait redis.set(key, JSON.stringify(payload));","handlingStrategy":"type-guard","validationCode":"const raw = await redis.get(key);\nif (raw !== null && typeof raw !== 'string') throw new Error(`non-string cache value at ${key}`);","typeGuard":"const isStringResult = (r) => r == null || typeof r.result === 'string';","tryCatchPattern":"try {\n  const payload = readCacheBatch(keys);\n} catch (err) {\n  if (err.message.includes('invalid result')) {\n    logger.warn({ err }, 'cache type mismatch; purging and reseeding');\n    await purgeKeys(keys);\n  } else throw err;\n}","preventionTips":["Always write cache values as JSON.stringify strings","Use the same Redis client and options for writer and reader","Audit key types (TYPE command) after client-library changes"],"tags":["cache","redis","type-mismatch"],"backgroundTag":"type-mismatch","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"}