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

  1. Check the fastify error log for the underlying exception
  2. Confirm the basket slug exists and has computed data for the requested market
  3. Verify DATABASE_URL and database reachability
  4. 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

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


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/d52f8d509d35961b. Report an issue: GitHub.