koala73/worldmonitor · critical

Redis unavailable

Error message

Redis unavailable

What it means

geoSearchByBoxStrict is the strict variant of the Upstash Redis GEOSEARCH helper in server/_shared/redis.ts. It reads UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN from the environment and throws immediately when either is unset, because a strict geo query cannot proceed without REST credentials.

Solutions

  1. Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in the environment (or .env loaded via loadEnvFile()).
  2. Verify the variable names match exactly (case-sensitive) and contain non-empty values.
  3. Restart/redeploy the process after adding the env vars so they are picked up.
  4. Use the non-strict variant or return an empty/degraded result if the caller should tolerate a missing Redis configuration.

Example fix

// before
const result = await geoSearchByBoxStrict('vessels:geo', lon, lat, 50, 50, 100);
// after
if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) {
  return []; // degraded mode
}
const result = await geoSearchByBoxStrict('vessels:geo', lon, lat, 50, 50, 100);
Defensive patterns

Strategy: fallback

Validate before calling

export function hasRedisConfig(): boolean {
  return Boolean(process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN);
}

Type guard

const isRedisConfigured = (): boolean =>
  typeof process.env.UPSTASH_REDIS_REST_URL === 'string' &&
  process.env.UPSTASH_REDIS_REST_URL.length > 0 &&
  typeof process.env.UPSTASH_REDIS_REST_TOKEN === 'string' &&
  process.env.UPSTASH_REDIS_REST_TOKEN.length > 0;

Try / catch

let results: string[];
try {
  results = await geoSearchByBoxStrict(key, lon, lat, w, h, count);
} catch (err) {
  if (err instanceof Error && err.message === 'Redis unavailable') {
    results = []; // degraded mode without Redis
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling geoSearchByBoxStrict (directly or via a maritime/geospatial endpoint) in an environment where process.env.UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN is missing or empty — e.g. local dev without a .env loaded, a preview deployment without the secrets configured, or the worker process not going through loadEnvFile().

Common situations: Local development without Upstash credentials in .env; Vercel/Railway environment variables not set for the environment; env vars renamed or mistyped; tests running without secret injection.

Understand the failure class

Background: "environment variable is not set" and "Missing keys in environment" errors: what missing required env var messages mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22). Data as JSON: /api/errors/bcdaef7f5bdc493b. Report an issue: GitHub.

Appendix: source

Thrown at server/_shared/redis.ts:1213

  } catch (error) {
    logCacheReadError(args[0], error);
    return [];
  }
}

export async function getHashFieldsBatch(...args: Parameters<typeof getHashFieldsBatchStrict>): Promise<Map<string, string>> {
  try {
    return await getHashFieldsBatchStrict(...args);
  } catch (error) {
    logCacheReadError(args[0], error);
    return new Map();
  }
}

export async function geoSearchByBoxStrict(key: string, lon: number, lat: number, widthKm: number, heightKm: number, count: number, raw = false): Promise<string[]> {
  const url = process.env.UPSTASH_REDIS_REST_URL;
  const token = process.env.UPSTASH_REDIS_REST_TOKEN;
  if (!url || !token) throw new Error('Redis unavailable');
  const finalKey = raw ? key : prefixKey(key);
  const pipeline = [['GEOSEARCH', finalKey, 'FROMLONLAT', String(lon), String(lat), 'BYBOX', String(widthKm), String(heightKm), 'km', 'ASC', 'COUNT', String(count)]];
  const resp = await fetch(`${url}/pipeline`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      'User-Agent': 'worldmonitor-server/1.0 (redis)',
    },
    body: JSON.stringify(pipeline),
    signal: AbortSignal.timeout(REDIS_PIPELINE_TIMEOUT_MS),
  });
  if (!resp.ok) throw new Error(`Redis HTTP ${resp.status}`);
  const data = (await resp.json()) as Array<{ result?: string[]; error?: string }>;
  if (data[0]?.error || !Array.isArray(data[0]?.result) || !data[0].result.every(value => typeof value === 'string')) throw new Error('Invalid GEOSEARCH result');
  return data[0].result;
}

View on GitHub (pinned to e586b8b4b8)