koala73/worldmonitor · error · Error
PortWatch cache read returned incomplete results
Error message
PortWatch cache read returned incomplete results
What it means
Thrown by redisMgetJson in scripts/seed-portwatch-port-activity.mjs when the batched GET (MGET-style pipeline) result array is missing or has a different length than the requested keys. redisMgetJson primes the per-country cache in one round-trip and treats a length mismatch as a corrupted response it cannot map back to keys.
Solutions
- Verify redisPipeline's success path — this throw means its length check passed upstream or was bypassed; inspect the actual results array logged before this point
- Reduce batch size (chunk keys into groups of ~50-100) to rule out truncation of large pipelines
- Retry the whole MGET with backoff since partial truncation is usually transient
- Confirm the endpoint is the /pipeline endpoint returning one result per command
- Add logging of keys.length vs results.length to identify which response shape reaches this check
Example fix
// before
const results = await redisPipeline(commands);
if (!Array.isArray(results) || results.length !== keys.length) {
throw new Error('PortWatch cache read returned incomplete results');
}
// after
const results = await mgetWithRetry(commands, { retries: 2, chunkSize: 100 });
if (!Array.isArray(results) || results.length !== keys.length) {
throw new Error(`PortWatch cache read incomplete: got ${results?.length}/${keys.length} results`);
} Defensive patterns
Strategy: retry
Validate before calling
const keysAreValidStrings = (keys) => Array.isArray(keys) && keys.length > 0 && keys.every((k) => typeof k === 'string' && k.length > 0);
Type guard
const isCompleteMgetResult = (res, n) => Array.isArray(res) && res.length === n;
Try / catch
try {
const cache = await redisMgetJson(keys);
} catch (err) {
if (err.message.includes('incomplete results')) {
return retryWithBackoff(() => redisMgetJson(keys), { retries: 2 });
}
throw err;
} Prevention
- Chunk MGET batches (50-100 keys) instead of one giant pipeline
- Retry transient short responses with backoff before failing the seed
- Log keys.length vs results.length on mismatch for diagnosis
- Pin and monitor the Upstash API behavior used by the seed
When it happens
Trigger: The underlying redisPipeline call returns null/undefined (caught and rethrown earlier usually), a non-array, or an array whose length differs from keys.length — e.g. a partial/interleaved response, a proxy truncating the body, or redisPipeline's own error path returning a short result list.
Common situations: Upstash silently truncating very large pipelines (174+ keys); a middlebox returning partial JSON; a bug in redisPipeline's response validation letting a malformed body through; mixing an old cached client with a new response format.
Related errors
- PortWatch cache read failed
- cache_all_null
- cache_all_null
- Failed to persist resilience score generation for ${generati
- Redis transaction failed: HTTP ${resp.status} — ${text.slice
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/9b602412a6332bdf.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/seed-portwatch-port-activity.mjs:1037
if (failures.length > 0) {
throw new Error(`Redis transaction: ${failures.length}/${commands.length} commands failed`);
}
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 }.
//View on GitHub (pinned to 7d06c8633d)