koala73/worldmonitor · error · Error
PortWatch cache read returned invalid result
Error message
PortWatch cache read returned invalid result
What it means
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).
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
Example fix
// before await redis.set(key, payload); // after await redis.set(key, JSON.stringify(payload));
Defensive patterns
Strategy: type-guard
Validate before calling
const raw = await redis.get(key);
if (raw !== null && typeof raw !== 'string') throw new Error(`non-string cache value at ${key}`); Type guard
const isStringResult = (r) => r == null || typeof r.result === 'string';
Try / catch
try {
const payload = readCacheBatch(keys);
} catch (err) {
if (err.message.includes('invalid result')) {
logger.warn({ err }, 'cache type mismatch; purging and reseeding');
await purgeKeys(keys);
} else throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- cache_all_null
- cache_all_null
- Failed to persist resilience score generation for ${generati
- PortWatch cache read returned incomplete results
- PortWatch cache read failed
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/90bf6997a4698cad.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/seed-portwatch-port-activity.mjs:1042
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.
//
// Orders cold-fetches by the oldest ATTEMPT, using the last successful cache
// write as the legacy fallback. This is the durable rotation cursor:View on GitHub (pinned to 7d06c8633d)