{"record":{"id":"d795070a70601b0d","repo":"koala73/worldmonitor","slug":"physical-premium-key","errorCode":null,"errorMessage":"${PHYSICAL_PREMIUM_KEY}","messagePattern":"\\$\\{PHYSICAL_PREMIUM_KEY\\}","errorType":"exception","errorClass":"SeedUnavailableError","httpStatus":503,"severity":"error","filePath":"server/worldmonitor/market/v1/get-physical-premiums.ts","lineNumber":174,"sourceCode":"    }\n    if (!metals.includes(metal)) metals.push(metal);\n  }\n  if (unsupported.length > 0) {\n    throw new ValidationError([{\n      field: 'metals',\n      description: `Unsupported metal${unsupported.length === 1 ? '' : 's'}: ${unsupported.join(', ')}`,\n    }]);\n  }\n  return metals;\n}\n\nexport async function getPhysicalPremiums(\n  _ctx: ServerContext,\n  req: GetPhysicalPremiumsRequest,\n): Promise<GetPhysicalPremiumsResponse> {\n  const metals = resolvePhysicalPremiumMetals(parseStringArray(req.metals));\n  const read = await readCachedJson(PHYSICAL_PREMIUM_KEY, true);\n  if (read.status === 'error') throw new SeedUnavailableError(PHYSICAL_PREMIUM_KEY);\n  try {\n    const raw = read.status === 'hit' ? read.value as RawPayload | null : null;\n    const fx = mapFx(raw?.fx);\n    if (!fx || !Array.isArray(raw?.premiums)) return { premiums: [] };\n    const premiums = raw.premiums.map((premium) => mapPremium(premium, fx.rate))\n      .filter((item): item is PhysicalPremium => item !== null);\n    if (premiums.length !== 2 || premiums.length !== raw.premiums.length) return { premiums: [] };\n    if (new Set(premiums.map((premium) => premium.metal)).size !== 2) return { premiums: [] };\n    const wanted = new Set(metals);\n    return {\n      premiums: metals.length === 0 ? premiums : premiums.filter((item) => wanted.has(item.metal)),\n      fx,\n    };\n  } catch {\n    return { premiums: [] };\n  }\n}\n","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/server/worldmonitor/market/v1/get-physical-premiums.ts#L156-L192","documentation":"getPhysicalPremiums throws SeedUnavailableError(PHYSICAL_PREMIUM_KEY) when readCachedJson reports an error status reading the physical premiums cache — i.e. Redis itself failed (connection, timeout, decode), not merely a miss. A clean miss instead degrades to an empty premiums list.","triggerScenarios":"Redis/Upstash connection failure, timeout, auth error, or deserialization error while reading PHYSICAL_PREMIUM_KEY; e.g. wrong credentials, Redis maintenance, network partition from the worker.","commonSituations":"Missing or rotated Upstash credentials on a new environment; Upstash free-tier eviction/limits; transient network blips during the read.","solutions":["Verify Upstash URL/token via loadEnvFile and test connectivity with a PING","Check logCacheReadError/readCachedJson error details for the Redis failure cause","Retry the request — transient Redis errors clear on their own","Confirm the seed producer is healthy so the key exists with a valid payload"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"const probe = await redis.ping();\nif (probe !== 'PONG') throw new Error('redis unreachable before physical premiums read');","typeGuard":null,"tryCatchPattern":"try {\n  return await api.getPhysicalPremiums(req);\n} catch (e) {\n  if (isSeedUnavailableError(e)) {\n    // Redis read error: brief retry, then degrade to empty premiums\n    return await withRetry(() => api.getPhysicalPremiums(req), { attempts: 2, fallback: { premiums: [] } });\n  }\n  throw e;\n}","preventionTips":["Validate Upstash credentials in every environment via loadEnvFile","Monitor Redis error rates (timeouts, auth, eviction)","Ensure the physical premiums producer keeps the key warm","Distinguish cache misses (empty result) from cache read errors"],"tags":["cache","redis","connectivity","seed"],"backgroundTag":"seed-cache-unavailable","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-22T01:50:49.965Z","contentChangedAt":"2026-09-22T01:50:49.965Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}