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

  1. 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
  2. Reduce batch size (chunk keys into groups of ~50-100) to rule out truncation of large pipelines
  3. Retry the whole MGET with backoff since partial truncation is usually transient
  4. Confirm the endpoint is the /pipeline endpoint returning one result per command
  5. 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

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


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)