koala73/worldmonitor · error · Error

Redis returned malformed JSON

Error message

Redis returned malformed JSON

What it means

Thrown by `parseStoredJson` in api/oauth/_refresh-recovery.ts:221 when the stored string under the refresh key (or the family pointer) is not valid JSON. The recovery protocol stores `JSON.stringify`-ed objects (refresh data, family pointers), so a JSON.parse failure means the stored payload was corrupted or written by a different producer/format.

Source

Thrown at api/oauth/_refresh-recovery.ts:221

    },
    body: JSON.stringify(['EVAL', script, String(keys.length), ...keys, ...args]),
    signal: AbortSignal.timeout(3_000),
  });
  if (!resp.ok) throw new Error(`Redis HTTP ${resp.status}`);
  const data = (await resp.json().catch(() => null)) as { result?: unknown; error?: string } | null;
  if (data?.error) throw new Error(`Redis EVAL failed: ${data.error}`);
  if (!data || !Object.prototype.hasOwnProperty.call(data, 'result')) {
    throw new Error('Redis EVAL returned an invalid response');
  }
  return data.result;
}

function parseStoredJson(value: unknown): unknown {
  if (typeof value !== 'string') throw new Error('Redis returned an invalid stored value');
  try {
    return JSON.parse(value);
  } catch {
    throw new Error('Redis returned malformed JSON');
  }
}

export async function rawRedisBeginRefreshAttempt(
  refreshToken: string,
  attemptId: string,
): Promise<RefreshConsumeResult> {
  const refreshKey = `oauth:refresh:${refreshToken}`;
  const attemptKey = refreshFamilyAttemptKey(refreshToken);
  const pointerKey = refreshFamilyPointerKey(refreshToken);
  const attemptValue = JSON.stringify({ attempt_id: attemptId });
  const attemptMarker = serializeRefreshAttemptMarker(attemptValue);
  const script = [
    "local value = redis.call('GET', KEYS[1])",
    'if value then',
    '  local ok, decoded = pcall(cjson.decode, value)',
    "  if ok and type(decoded) == 'table' and decoded.kind == 'refresh_attempt' then",
    "    return {0, redis.call('EXISTS', KEYS[2]), redis.call('GET', KEYS[3]) or false}",

View on GitHub (pinned to a96956387a)

Solutions

  1. GET the offending key and inspect the raw string; if it is not the expected JSON object, delete it (the token will simply require re-login) so the next refresh writes a fresh value
  2. If keys come from an old format, write a one-time migration or let TTL (REFRESH_TTL_SECONDS) age them out
  3. Ensure no other producer writes these keys with a different serialization
Defensive patterns

Strategy: try-catch

Type guard

function isMalformedRedisJson(e: unknown): boolean {
  return e instanceof Error && e.message === 'Redis returned malformed JSON';
}

Try / catch

try {
  await beginRefreshAttempt(token, id);
} catch (e) {
  if (isMalformedRedisJson(e)) {
    // non-retryable: delete the offending key and force re-login; log the raw value for forensics
  } else throw e;
}

Prevention

When it happens

Trigger: `oauth:refresh:<token>` or the family pointer key containing a hand-edited string, truncated data, or a legacy non-JSON format when `rawRedisBeginRefreshAttempt` consumes it and calls `parseStoredJson`.

Common situations: Legacy keys from an older serialization format surviving a deploy; manual console edits; truncation from exceeding value size limits; cross-format version skew after a migration.

Understand the failure class

Related errors


AI-assisted analysis of koala73/worldmonitor@a96956387a (2026-08-27). Data as JSON: /api/errors/81c8bc7fcea4b498. Report an issue: GitHub.