{"record":{"id":"9025ee2a0d88a70e","repo":"koala73/worldmonitor","slug":"failed-to-build-basket-series-snapshot","errorCode":null,"errorMessage":"failed to build basket series snapshot","messagePattern":"failed to build basket series snapshot","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"consumer-prices-core/src/api/routes/worldmonitor.ts","lineNumber":93,"sourceCode":"      return reply.send(data);\n    } catch (err) {\n      fastify.log.error(err);\n      return reply.status(500).send({ error: 'failed to build categories snapshot' });\n    }\n  });\n\n  fastify.get('/basket-series', async (request, reply) => {\n    const { market = 'ae', basket = 'essentials-ae', range = '30d' } = request.query as {\n      market?: string;\n      basket?: string;\n      range?: string;\n    };\n    try {\n      const data = await buildBasketSeriesSnapshot(market, basket, range);\n      return reply.send(data);\n    } catch (err) {\n      fastify.log.error(err);\n      return reply.status(500).send({ error: 'failed to build basket series snapshot' });\n    }\n  });\n}\n","sourceCodeStart":75,"sourceCodeEnd":97,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/api/routes/worldmonitor.ts#L75-L97","documentation":"The Fastify route GET /worldmonitor/basket-series returned 500 because buildBasketSeriesSnapshot(market, basket, range) threw. It is the most parameter-heavy snapshot route: market (default 'ae'), basket (default 'essentials-ae') and range (default '30d') all flow into the time-series SQL through the pg pool in src/db/client.ts. The 500 wraps a database throw (missing DATABASE_URL, unreachable Postgres, SQL regression) or an unsupported market/basket/range combination.","triggerScenarios":"DATABASE_URL unset or Postgres unreachable; a basket slug with no series data for the market; a range value the series date-bucketing cannot parse; a migration breaking the series query.","commonSituations":"Clients composing arbitrary range strings ('6m', 'ytd') the builder never supported; deployment missing the DB secret; the basket series job not having run for a new market.","solutions":["Validate all three params client-side against the supported set before calling the route","Check the fastify error log for the underlying exception","Verify the basket slug exists for the market and has series rows","Reproduce with buildBasketSeriesSnapshot(market, basket, range) directly"],"exampleFix":"// before — arbitrary range flows straight into the SQL layer\nconst data = await buildBasketSeriesSnapshot(market, basket, range);\n\n// after — constrain params before the builder runs\nconst RANGES = new Set(['7d', '30d', '90d']);\nif (!RANGES.has(range)) {\n  return reply.status(400).send({ error: `range must be one of ${[...RANGES].join(', ')}` });\n}\nconst data = await buildBasketSeriesSnapshot(market, basket, range);","handlingStrategy":"validation","validationCode":"const VALID_RANGES = new Set(['7d', '30d', '90d']);\nconst VALID_BASKETS: Record<string, string[]> = { ae: ['essentials-ae'] };\nif (!VALID_BASKETS[market]?.includes(basket)) throw new Error(`basket '${basket}' invalid for market '${market}'`);\nif (!VALID_RANGES.has(range)) throw new Error(`range '${range}' unsupported`);\nconst res = await fetch(`/worldmonitor/basket-series?market=${market}&basket=${basket}&range=${range}`);","typeGuard":"function isValidSeriesRequest(q: { market?: string; basket?: string; range?: string }): boolean {\n  return !!q.market\n    && (VALID_BASKETS[q.market] ?? []).includes(q.basket ?? 'essentials-ae')\n    && VALID_RANGES.has(q.range ?? '30d');\n}","tryCatchPattern":"try {\n  const res = await fetch(`/worldmonitor/basket-series?market=${market}&basket=${basket}&range=${range}`);\n  if (!res.ok) throw new Error(`basket-series: HTTP ${res.status}`);\n  return await res.json();\n} catch (err) {\n  if (/HTTP 500/.test(String(err))) return readCachedSeries(market, basket, range); // last good series\n  throw err;\n}","preventionTips":["Validate all three params (market, basket, range) against supported sets before the request","Cache series snapshots — time-series data tolerates staleness well during backend incidents","Ensure the basket-series computation job has run for a market before its routes go live"],"tags":["fastify","http-500","database","snapshot","time-series","query-params"],"backgroundTag":"database-query-failed","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-31T04:17:50.494Z"}