koala73/worldmonitor · error · Error

PortWatch cache read failed

Error message

PortWatch cache read failed

What it means

Thrown by redisMgetJson when an individual per-key result in the batched GET is malformed: it has an error property, or lacks a result property entirely. Each Upstash pipeline entry must be an object like {result: ...} or {error: ...}; anything else means the response entry cannot be interpreted as a GET outcome.

Solutions

  1. Log the failing entry (index and raw value) to see the embedded error or actual type
  2. Run TYPE on one of the failing keys to check for WRONGTYPE conflicts between writers
  3. Delete/flush the affected key namespace and re-run the seed to rewrite values as JSON strings
  4. Ensure all writers to these keys serialize with JSON.stringify (string values only)
  5. Align the key prefix/schema with the reader so no foreign keys are requested

Example fix

// before
return results.map((r) => {
  if (r?.error || !Object.hasOwn(r ?? {}, 'result')) throw new Error('PortWatch cache read failed');
// after
return results.map((r, i) => {
  if (r?.error || !Object.hasOwn(r ?? {}, 'result')) {
    throw new Error(`PortWatch cache read failed for key ${keys[i]}: ${JSON.stringify(r)?.slice(0, 120)}`);
  }
Defensive patterns

Strategy: validation

Validate before calling

const keyResultsAreWellFormed = (results) => Array.isArray(results) && results.every((r) => r !== null && typeof r === 'object' && (Object.hasOwn(r, 'result') || Object.hasOwn(r, 'error')));

Type guard

const isUpstashGetEntry = (r) => typeof r === 'object' && r !== null && Object.hasOwn(r, 'result') && !r.error;

Try / catch

try {
  const cache = await redisMgetJson(keys);
} catch (err) {
  if (err.message.includes('cache read failed')) {
    console.error('Entry-level Upstash error (check for WRONGTYPE): inspect key types before rerun');
    throw err;
  }
  throw err;
}

Prevention

When it happens

Trigger: One or more entries of the Upstash /pipeline response contain {error:'...'} (e.g. WRONGTYPE if the key holds a non-string value like a hash or list), or a null/undefined entry where {result:...} was expected — often from an API change or a key written by a different code path with a different type.

Common situations: Keys written by an older seed version as hashes/lists instead of JSON strings; Upstash returning command-level errors inside an otherwise-200 pipeline; stale/mismatched database where the key namespace holds different data types; typo'd key prefix hitting entries of another feature.

Related errors


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

Appendix: source

Thrown at scripts/seed-portwatch-port-activity.mjs:1040

  return results;
}

const CORRUPT_COUNTRY_CACHE = Symbol('corrupt country cache');

// MGET-style batch read via the Upstash REST /pipeline endpoint. Returns an
// array aligned with `keys` where each element is either the parsed JSON
// payload, explicit miss, or confirmed corrupt value. Transport/envelope errors
// remain fatal: only a validated upstream replacement may overwrite corruption.
// Primes the per-country cache lookup in one round-trip instead of 174 GETs.
async function redisMgetJson(keys) {
  if (keys.length === 0) return [];
  const commands = keys.map((k) => ['GET', k]);
  const results = await redisPipeline(commands);
  if (!Array.isArray(results) || results.length !== keys.length) {
    throw new Error('PortWatch cache read returned incomplete results');
  }
  return results.map((r) => {
    if (r?.error || !Object.hasOwn(r ?? {}, 'result')) throw new Error('PortWatch cache read failed');
    if (r.result === null) return null;
    if (typeof r.result !== 'string') throw new Error('PortWatch cache read returned invalid result');
    try {
      const payload = JSON.parse(r.result);
      return payload && typeof payload === 'object' && !Array.isArray(payload)
        ? payload : CORRUPT_COUNTRY_CACHE;
    } catch {
      return CORRUPT_COUNTRY_CACHE;
    }
  });
}

// fetchAll() — pure data collection, no Redis writes.
// Returns { countries: string[], countryData: Map<iso2, payload>, fetchedAt: string }.
//
// `progress` (optional) is mutated in-place so a SIGTERM handler in main()
// can report which batch / country we died on.
//

View on GitHub (pinned to 7d06c8633d)