{"record":{"id":"07f5c52b3624f6e3","repo":"koala73/worldmonitor","slug":"failed-to-build-categories-snapshot","errorCode":null,"errorMessage":"failed to build categories snapshot","messagePattern":"failed to build categories snapshot","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"consumer-prices-core/src/api/routes/worldmonitor.ts","lineNumber":78,"sourceCode":"  fastify.get('/freshness', async (request, reply) => {\n    const { market = 'ae' } = request.query as { market?: string };\n    try {\n      const data = await buildFreshnessSnapshot(market);\n      return reply.send(data);\n    } catch (err) {\n      fastify.log.error(err);\n      return reply.status(500).send({ error: 'failed to build freshness snapshot' });\n    }\n  });\n\n  fastify.get('/categories', async (request, reply) => {\n    const { market = 'ae', range = '30d' } = request.query as { market?: string; range?: string };\n    try {\n      const data = await buildCategoriesSnapshot(market, range);\n      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}","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/api/routes/worldmonitor.ts#L60-L96","documentation":"The Fastify route GET /worldmonitor/categories returned 500 because buildCategoriesSnapshot(market, range) threw. Inputs are market (default 'ae') and range (default '30d'); the builder computes per-category WoW/MoM deltas and sparklines via the pg pool in src/db/client.ts. The 500 wraps a database-side throw — missing DATABASE_URL, connection failure, SQL regression — or a range value the date-bucketing logic cannot handle.","triggerScenarios":"DATABASE_URL unset; Postgres unreachable; a range string like '7d'/'90d' hitting unsupported date math in the builder; a migration breaking the category aggregate; pool exhaustion.","commonSituations":"Client sending an ad-hoc range value the builder never anticipated; deployment missing the DB secret; category stats absent for a newly onboarded market.","solutions":["Check the fastify error log for the underlying exception","Retry with the default range ('30d') to determine whether the range value or the database is at fault","Verify DATABASE_URL and database reachability","Reproduce with buildCategoriesSnapshot(market, range) directly"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"const VALID_RANGES = new Set(['7d', '30d', '90d']);\nconst range = params.get('range') ?? '30d';\nif (!VALID_RANGES.has(range)) {\n  throw new Error(`range '${range}' unsupported — use one of ${[...VALID_RANGES].join(', ')}`);\n}\nconst res = await fetch(`/worldmonitor/categories?market=ae&range=${range}`);","typeGuard":null,"tryCatchPattern":"const res = await fetch(`/worldmonitor/categories?market=${market}&range=${range}`);\nif (res.status === 500) {\n  // first rule out a bad range by retrying the default\n  const fallbackRes = await fetch(`/worldmonitor/categories?market=${market}&range=30d`);\n  if (fallbackRes.ok) return await fallbackRes.json();\n  throw new Error('categories snapshot backend down');\n}\nif (!res.ok) throw new Error(`categories: HTTP ${res.status}`);","preventionTips":["Constrain range values to a published set instead of forwarding arbitrary strings into date math","Confirm category stats exist for newly onboarded markets before exposing them","Treat simultaneous 500s across snapshot routes as a database incident, not per-route bugs"],"tags":["fastify","http-500","database","snapshot","categories"],"backgroundTag":"database-query-failed","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-31T04:17:50.494Z"}