koala73/worldmonitor · error
failed to build retailer spread snapshot
Error message
failed to build retailer spread snapshot
What it means
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.
Source
Thrown at consumer-prices-core/src/api/routes/worldmonitor.ts:56
const data = await buildMoversSnapshot(market, parseInt(days, 10));
return reply.send(data);
} catch (err) {
fastify.log.error(err);
return reply.status(500).send({ error: 'failed to build movers snapshot' });
}
});
fastify.get('/retailer-spread', async (request, reply) => {
const { market = 'ae', basket = 'essentials-ae' } = request.query as {
market?: string;
basket?: string;
};
try {
const data = await buildRetailerSpreadSnapshot(market, basket);
return reply.send(data);
} catch (err) {
fastify.log.error(err);
return reply.status(500).send({ error: 'failed to build retailer spread snapshot' });
}
});
fastify.get('/freshness', async (request, reply) => {
const { market = 'ae' } = request.query as { market?: string };
try {
const data = await buildFreshnessSnapshot(market);
return reply.send(data);
} catch (err) {
fastify.log.error(err);
return reply.status(500).send({ error: 'failed to build freshness snapshot' });
}
});
fastify.get('/categories', async (request, reply) => {
const { market = 'ae', range = '30d' } = request.query as { market?: string; range?: string };
try {
const data = await buildCategoriesSnapshot(market, range);View on GitHub (pinned to eeab0a219f)
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
Defensive patterns
Strategy: retry
Validate before calling
// client-side: only request baskets that exist for the market
const VALID_BASKETS: Record<string, string[]> = { ae: ['essentials-ae'] };
if (!VALID_BASKETS[market]?.includes(basket)) {
throw new Error(`basket '${basket}' not available for market '${market}'`);
} Try / catch
const res = await fetch(`/worldmonitor/retailer-spread?market=${market}&basket=${basket}`);
if (res.status === 500) {
await sleep(1_000);
return await fetch(`/worldmonitor/retailer-spread?market=${market}&basket=${basket}`);
}
if (!res.ok) throw new Error(`retailer-spread: HTTP ${res.status}`); Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.
Related errors
- failed to build coverage snapshot
- failed to build overview snapshot
- failed to build movers snapshot
- failed to build freshness snapshot
- failed to build categories snapshot
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/d52f8d509d35961b.
Report an issue: GitHub.