koala73/worldmonitor · error

failed to build basket series snapshot

Error message

failed to build basket series snapshot

What it means

The Fastify route GET /worldmonitor/basket-series returned 500 because buildBasketSeriesSnapshot(market, basket, range) threw. It is the most parameter-heavy snapshot route: market (default 'ae'), basket (default 'essentials-ae') and range (default '30d') all flow into the time-series SQL through the pg pool in src/db/client.ts. The 500 wraps a database throw (missing DATABASE_URL, unreachable Postgres, SQL regression) or an unsupported market/basket/range combination.

Source

Thrown at consumer-prices-core/src/api/routes/worldmonitor.ts:93

      return reply.send(data);
    } catch (err) {
      fastify.log.error(err);
      return reply.status(500).send({ error: 'failed to build categories snapshot' });
    }
  });

  fastify.get('/basket-series', async (request, reply) => {
    const { market = 'ae', basket = 'essentials-ae', range = '30d' } = request.query as {
      market?: string;
      basket?: string;
      range?: string;
    };
    try {
      const data = await buildBasketSeriesSnapshot(market, basket, range);
      return reply.send(data);
    } catch (err) {
      fastify.log.error(err);
      return reply.status(500).send({ error: 'failed to build basket series snapshot' });
    }
  });
}

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Validate all three params client-side against the supported set before calling the route
  2. Check the fastify error log for the underlying exception
  3. Verify the basket slug exists for the market and has series rows
  4. Reproduce with buildBasketSeriesSnapshot(market, basket, range) directly

Example fix

// before — arbitrary range flows straight into the SQL layer
const data = await buildBasketSeriesSnapshot(market, basket, range);

// after — constrain params before the builder runs
const RANGES = new Set(['7d', '30d', '90d']);
if (!RANGES.has(range)) {
  return reply.status(400).send({ error: `range must be one of ${[...RANGES].join(', ')}` });
}
const data = await buildBasketSeriesSnapshot(market, basket, range);
Defensive patterns

Strategy: validation

Validate before calling

const VALID_RANGES = new Set(['7d', '30d', '90d']);
const VALID_BASKETS: Record<string, string[]> = { ae: ['essentials-ae'] };
if (!VALID_BASKETS[market]?.includes(basket)) throw new Error(`basket '${basket}' invalid for market '${market}'`);
if (!VALID_RANGES.has(range)) throw new Error(`range '${range}' unsupported`);
const res = await fetch(`/worldmonitor/basket-series?market=${market}&basket=${basket}&range=${range}`);

Type guard

function isValidSeriesRequest(q: { market?: string; basket?: string; range?: string }): boolean {
  return !!q.market
    && (VALID_BASKETS[q.market] ?? []).includes(q.basket ?? 'essentials-ae')
    && VALID_RANGES.has(q.range ?? '30d');
}

Try / catch

try {
  const res = await fetch(`/worldmonitor/basket-series?market=${market}&basket=${basket}&range=${range}`);
  if (!res.ok) throw new Error(`basket-series: HTTP ${res.status}`);
  return await res.json();
} catch (err) {
  if (/HTTP 500/.test(String(err))) return readCachedSeries(market, basket, range); // last good series
  throw err;
}

Prevention

When it happens

Trigger: DATABASE_URL unset or Postgres unreachable; a basket slug with no series data for the market; a range value the series date-bucketing cannot parse; a migration breaking the series query.

Common situations: Clients composing arbitrary range strings ('6m', 'ytd') the builder never supported; deployment missing the DB secret; the basket series job not having run for a new market.

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/9025ee2a0d88a70e. Report an issue: GitHub.