koala73/worldmonitor · error
Invalid GEOSEARCH result
Error message
Invalid GEOSEARCH result
What it means
After sending the GEOSEARCH pipeline command to the Upstash REST API, the helper validates the response shape: it must be an array whose first entry has no error field and a result that is an array of strings. If the response is malformed, contains a per-command error, or has an unexpected element type, the helper throws instead of returning untrusted data as search results.
Solutions
- Log data[0].error to see the underlying Redis error and fix the key's type (delete and repopulate with GEOADD).
- Confirm the key holds a geospatial set (TYPE <key> == zset populated via GEOADD) and that the name matches after prefixKey().
- Retry on transient upstream issues; check Upstash status/incidents if shapes changed suddenly.
- Harden the caller to catch this error and return an empty result set for degraded operation.
Example fix
// before
const members = await geoSearchByBoxStrict(key, lon, lat, w, h, 10);
// after
let members: string[] = [];
try {
members = await geoSearchByBoxStrict(key, lon, lat, w, h, 10);
} catch (err) {
if (err instanceof Error && err.message === 'Invalid GEOSEARCH result') {
console.error('geoSearchByBoxStrict failed for key', key, err);
} else throw err;
} Defensive patterns
Strategy: try-catch
Validate before calling
// pre-check the key type before GEOSEARCH (strict helper assumes a geo set)
const keyType = await redisType(finalKey);
if (keyType !== 'zset') throw new Error(`Key ${finalKey} is not a geo set (type: ${keyType})`); Type guard
type PipelineEntry = { result?: unknown; error?: string };
function isValidGeoResult(entry: unknown): entry is { result: string[] } {
const e = entry as PipelineEntry;
return !e.error && Array.isArray(e.result) && e.result.every((v): v is string => typeof v === 'string');
} Try / catch
try {
const members = await geoSearchByBoxStrict(key, lon, lat, w, h, count);
} catch (err) {
if (err instanceof Error && err.message === 'Invalid GEOSEARCH result') {
logger.error({ key, err }, 'GEOSEARCH returned unexpected shape; serving empty result');
return [];
}
throw err;
} Prevention
- Only pass keys populated with GEOADD; mixing ZADD/SET into the same key triggers pipeline errors.
- Log the full pipeline response when this error fires to surface the embedded Redis error string.
- Watch Upstash changelogs/status for pipeline response format changes.
- Add a fixture-based test that mocks a malformed pipeline response.
When it happens
Trigger: Upstash returns 200 but the pipeline body indicates an error (e.g. WRONGTYPE because the key holds a non-geo SET, or NOGROUP/key-type errors); the JSON parses into an unexpected shape (null entry, result missing, numeric members); a proxy/gateway intercepts and returns HTML or a different JSON schema.
Common situations: GEOSEARCH executed against a key that was populated with ZADD or SET instead of GEOADD; Upstash incident or API change altering the pipeline response envelope; a CDN/error page returned with a 200 status; mistyped key colliding with a different data structure after prefixKey().
Related errors
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-09-22).
Data as JSON: /api/errors/e57c6ba7a8546a39.
Report an issue: GitHub.
Appendix: source
Thrown at server/_shared/redis.ts:1228
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;
}
export async function getHashFieldsBatchStrict(
key: string,
fields: string[],
raw = false,
timeoutMs?: number,
): Promise<Map<string, string>> {
const result = new Map<string, string>();
if (fields.length === 0) return result;
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 = [['HMGET', finalKey, ...fields]];
const resp = await fetch(`${url}/pipeline`, {
method: 'POST',View on GitHub (pinned to e586b8b4b8)