{"record":{"id":"29018eeec510f265","repo":"koala73/worldmonitor","slug":"failed-to-build-coverage-snapshot","errorCode":null,"errorMessage":"failed to build coverage snapshot","messagePattern":"failed to build coverage snapshot","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"consumer-prices-core/src/api/routes/worldmonitor.ts","lineNumber":20,"sourceCode":"import { buildCoverageSnapshot } from '../../snapshots/coverage.js';\nimport {\n  buildBasketSeriesSnapshot,\n  buildCategoriesSnapshot,\n  buildFreshnessSnapshot,\n  buildMoversSnapshot,\n  buildOverviewSnapshot,\n  buildRetailerSpreadSnapshot,\n} from '../../snapshots/worldmonitor.js';\n\nexport async function worldmonitorRoutes(fastify: FastifyInstance) {\n  fastify.get('/coverage', async (request, reply) => {\n    const { market = 'ae' } = request.query as { market?: string };\n    try {\n      const data = await buildCoverageSnapshot(market);\n      return reply.send(data);\n    } catch (err) {\n      fastify.log.error(err);\n      return reply.status(500).send({ error: 'failed to build coverage snapshot' });\n    }\n  });\n\n  fastify.get('/overview', async (request, reply) => {\n    const { market = 'ae' } = request.query as { market?: string };\n    try {\n      const data = await buildOverviewSnapshot(market);\n      return reply.send(data);\n    } catch (err) {\n      fastify.log.error(err);\n      return reply.status(500).send({ error: 'failed to build overview snapshot' });\n    }\n  });\n\n  fastify.get('/movers', async (request, reply) => {\n    const { market = 'ae', days = '30' } = request.query as { market?: string; days?: string };\n    try {\n      const data = await buildMoversSnapshot(market, parseInt(days, 10));","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/api/routes/worldmonitor.ts#L2-L38","documentation":"The Fastify route GET /worldmonitor/coverage (query market, default 'ae') returned 500 because buildCoverageSnapshot(market) threw. The builder aggregates coverage SQL through the pg pool in src/db/client.ts, so in practice the throw is almost always database-side: DATABASE_URL unset (getPool throws 'DATABASE_URL is not set'), a connection failure (5s connect timeout), or a SQL/shape error. The route logs the real cause via fastify.log.error and returns only this generic string.","triggerScenarios":"API process started without DATABASE_URL; Postgres unreachable or credentials wrong (pool connect timeout 5000ms); a market value hitting a query path that throws; a SQL regression in buildCoverageSnapshot; pool exhaustion past max 10 connections.","commonSituations":"Deployed API missing its database secret; local run against a stopped Postgres; a migration renaming a column the snapshot query selects; load pushing past the 10-connection pool so queries queue and time out.","solutions":["Check the server logs — fastify.log.error printed the underlying cause immediately before the 500","Verify DATABASE_URL is set in the API process and points at a reachable Postgres","If the cause is SQL/shape, reproduce by calling buildCoverageSnapshot(market) directly with the same market","If pool exhaustion, raise the pg Pool max or batch snapshot callers"],"exampleFix":"// before — boot the API without checking the snapshot dependency\nawait app.listen({ port: 3000 });\n\n// after — fail fast when the database backing the route is missing\nif (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');\nawait query('SELECT 1');\nawait app.listen({ port: 3000 });","handlingStrategy":"retry","validationCode":"// startup preflight for every worldmonitor snapshot route\nasync function snapshotDepsReady(): Promise<boolean> {\n  if (!process.env.DATABASE_URL) return false;\n  try { await query('SELECT 1'); return true; } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"let data;\nfor (let attempt = 1; attempt <= 3; attempt++) {\n  const res = await fetch('/worldmonitor/coverage?market=ae');\n  if (res.ok) { data = await res.json(); break; }\n  if (res.status !== 500) throw new Error(`coverage: HTTP ${res.status}`);\n  await sleep(attempt * 1_000); // db-backed 500s are usually transient\n}","preventionTips":["Validate DATABASE_URL and run SELECT 1 before binding the API port","Monitor the fastify error log, not the 500 body — the body intentionally hides the cause","Keep snapshot queries covered by tests so SQL regressions fail in CI, not in the route"],"tags":["fastify","http-500","database","snapshot","coverage"],"backgroundTag":"database-query-failed","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-31T04:17:50.494Z"}