{"record":{"id":"81c8bc7fcea4b498","repo":"koala73/worldmonitor","slug":"redis-returned-malformed-json","errorCode":null,"errorMessage":"Redis returned malformed JSON","messagePattern":"Redis returned malformed JSON","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"api/oauth/_refresh-recovery.ts","lineNumber":221,"sourceCode":"    },\n    body: JSON.stringify(['EVAL', script, String(keys.length), ...keys, ...args]),\n    signal: AbortSignal.timeout(3_000),\n  });\n  if (!resp.ok) throw new Error(`Redis HTTP ${resp.status}`);\n  const data = (await resp.json().catch(() => null)) as { result?: unknown; error?: string } | null;\n  if (data?.error) throw new Error(`Redis EVAL failed: ${data.error}`);\n  if (!data || !Object.prototype.hasOwnProperty.call(data, 'result')) {\n    throw new Error('Redis EVAL returned an invalid response');\n  }\n  return data.result;\n}\n\nfunction parseStoredJson(value: unknown): unknown {\n  if (typeof value !== 'string') throw new Error('Redis returned an invalid stored value');\n  try {\n    return JSON.parse(value);\n  } catch {\n    throw new Error('Redis returned malformed JSON');\n  }\n}\n\nexport async function rawRedisBeginRefreshAttempt(\n  refreshToken: string,\n  attemptId: string,\n): Promise<RefreshConsumeResult> {\n  const refreshKey = `oauth:refresh:${refreshToken}`;\n  const attemptKey = refreshFamilyAttemptKey(refreshToken);\n  const pointerKey = refreshFamilyPointerKey(refreshToken);\n  const attemptValue = JSON.stringify({ attempt_id: attemptId });\n  const attemptMarker = serializeRefreshAttemptMarker(attemptValue);\n  const script = [\n    \"local value = redis.call('GET', KEYS[1])\",\n    'if value then',\n    '  local ok, decoded = pcall(cjson.decode, value)',\n    \"  if ok and type(decoded) == 'table' and decoded.kind == 'refresh_attempt' then\",\n    \"    return {0, redis.call('EXISTS', KEYS[2]), redis.call('GET', KEYS[3]) or false}\",","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/koala73/worldmonitor/blob/a96956387a927b8cd7aa34b0c41fca357e746be9/api/oauth/_refresh-recovery.ts#L203-L239","documentation":"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.","triggerScenarios":"`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`.","commonSituations":"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.","solutions":["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","If keys come from an old format, write a one-time migration or let TTL (REFRESH_TTL_SECONDS) age them out","Ensure no other producer writes these keys with a different serialization"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isMalformedRedisJson(e: unknown): boolean {\n  return e instanceof Error && e.message === 'Redis returned malformed JSON';\n}","tryCatchPattern":"try {\n  await beginRefreshAttempt(token, id);\n} catch (e) {\n  if (isMalformedRedisJson(e)) {\n    // non-retryable: delete the offending key and force re-login; log the raw value for forensics\n  } else throw e;\n}","preventionTips":["Use one serializer for all oauth:* values; version the payload format and migrate old keys","Never hand-edit stored refresh payloads in the Redis console"],"tags":["redis","json","data-corruption","oauth","refresh-token"],"backgroundTag":"corrupted-stored-value","analyzedSha":"a96956387a927b8cd7aa34b0c41fca357e746be9","analyzedAt":"2026-08-27T19:53:08.521Z","contentChangedAt":"2026-08-27T19:53:08.521Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}