{"record":{"id":"d52f8d509d35961b","repo":"koala73/worldmonitor","slug":"failed-to-build-retailer-spread-snapshot","errorCode":null,"errorMessage":"failed to build retailer spread snapshot","messagePattern":"failed to build retailer spread snapshot","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"consumer-prices-core/src/api/routes/worldmonitor.ts","lineNumber":56,"sourceCode":"      const data = await buildMoversSnapshot(market, parseInt(days, 10));\n      return reply.send(data);\n    } catch (err) {\n      fastify.log.error(err);\n      return reply.status(500).send({ error: 'failed to build movers snapshot' });\n    }\n  });\n\n  fastify.get('/retailer-spread', async (request, reply) => {\n    const { market = 'ae', basket = 'essentials-ae' } = request.query as {\n      market?: string;\n      basket?: string;\n    };\n    try {\n      const data = await buildRetailerSpreadSnapshot(market, basket);\n      return reply.send(data);\n    } catch (err) {\n      fastify.log.error(err);\n      return reply.status(500).send({ error: 'failed to build retailer spread snapshot' });\n    }\n  });\n\n  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);","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/consumer-prices-core/src/api/routes/worldmonitor.ts#L38-L74","documentation":"The Fastify route GET /worldmonitor/retailer-spread returned 500 because buildRetailerSpreadSnapshot(market, basket) threw. Inputs are market (default 'ae') and basket (default 'essentials-ae'); the builder compares retailer basket totals via the pg pool in src/db/client.ts. The 500 therefore wraps a database-side or basket-data throw: missing DATABASE_URL, connection failure, a SQL regression, or a basket slug with no computed spread data hitting throwing code.","triggerScenarios":"DATABASE_URL unset or Postgres unreachable; a basket slug that does not exist or has no computed totals; a migration breaking the spread aggregate; pool exhaustion at max 10 connections.","commonSituations":"Calling with a basket slug from a different market ('essentials-uk' against market 'ae'); deployment missing the DB secret; the basket computation job not having run yet.","solutions":["Check the fastify error log for the underlying exception","Confirm the basket slug exists and has computed data for the requested market","Verify DATABASE_URL and database reachability","Reproduce with buildRetailerSpreadSnapshot(market, basket) directly"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// client-side: only request baskets that exist for the market\nconst VALID_BASKETS: Record<string, string[]> = { ae: ['essentials-ae'] };\nif (!VALID_BASKETS[market]?.includes(basket)) {\n  throw new Error(`basket '${basket}' not available for market '${market}'`);\n}","typeGuard":null,"tryCatchPattern":"const res = await fetch(`/worldmonitor/retailer-spread?market=${market}&basket=${basket}`);\nif (res.status === 500) {\n  await sleep(1_000);\n  return await fetch(`/worldmonitor/retailer-spread?market=${market}&basket=${basket}`);\n}\nif (!res.ok) throw new Error(`retailer-spread: HTTP ${res.status}`);","preventionTips":["Publish the valid market/basket matrix to clients instead of letting them guess slugs","Serve the last computed spread snapshot from cache when the builder throws","Check the fastify error log for the underlying pg error before changing client code"],"tags":["fastify","http-500","database","snapshot","basket"],"backgroundTag":"database-query-failed","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-31T04:17:50.494Z"}