koala73/worldmonitor · error · Error

Redis transaction failed: HTTP ${resp.status} — ${text.slice

Error message

Redis transaction failed: HTTP ${resp.status} — ${text.slice(0, 200)}

What it means

Thrown by the Upstash Redis REST pipeline helper (redisPipeline) in scripts/seed-portwatch-port-activity.mjs when the HTTP response from the Redis REST endpoint has a non-2xx status. The thrown message embeds the HTTP status and the first 200 characters of the response body, which typically contains Upstash's error explanation (auth failure, rate limit, malformed command list).

Solutions

  1. Read the HTTP status and body snippet in the error message — 401/403 means credentials, 429 means throttling, 5xx means Upstash outage
  2. Verify UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN are loaded via loadEnvFile() and match the current database
  3. For 429, chunk the command list into smaller pipelines or add delay between requests
  4. Retry on 5xx/network errors with exponential backoff; do not retry 4xx auth errors
  5. Confirm network egress and that no proxy intercepts requests to the Upstash host

Example fix

// before
const resp = await fetch(url, { method: 'POST', body: JSON.stringify(commands), signal: AbortSignal.timeout(30_000) });
if (!resp.ok) {
  const text = await resp.text().catch(() => '');
  throw new Error(`Redis transaction failed: HTTP ${resp.status} — ${text.slice(0, 200)}`);
}
// after
let resp;
for (let attempt = 0; attempt < 3; attempt++) {
  resp = await fetch(url, { method: 'POST', body: JSON.stringify(chunk), signal: AbortSignal.timeout(30_000) });
  if (resp.ok) break;
  if (resp.status === 401 || resp.status === 403) {
    throw new Error(`Redis credentials rejected: HTTP ${resp.status}`);
  }
  await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
}
if (!resp?.ok) throw new Error(`Redis transaction failed after retries: HTTP ${resp?.status}`);
Defensive patterns

Strategy: retry

Validate before calling

function assertUpstashEnv() {
  if (!process.env.UPSTASH_REDIS_REST_URL || !process.env.UPSTASH_REDIS_REST_TOKEN) {
    throw new Error('UPSTASH_REDIS_REST_URL/TOKEN missing');
  }
  new URL(process.env.UPSTASH_REDIS_REST_URL);
}

Type guard

const isUpstashHttpError = (e) => e instanceof Error && e.message.startsWith('Redis transaction failed: HTTP');

Try / catch

try {
  await redisPipeline(commands);
} catch (err) {
  if (/Redis transaction failed: HTTP 4(01|03)/.test(err.message)) {
    throw new Error('Upstash credentials invalid — fix env and abort');
  }
  if (/HTTP 429|HTTP 5\d\d/.test(err.message)) {
    return retryWithBackoff(() => redisPipeline(commands));
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling redisPipeline (used by redisMgetJson and cache writes) when the Upstash REST API returns 401 (bad token), 403, 429 (rate/QPS limit exceeded by the 174-key pipeline), 5xx outage, or a proxy/CDN error page instead of a JSON transaction result.

Common situations: Wrong or rotated UPSTASH_REDIS_REST_URL/TOKEN in env; free-tier QPS limits hit by large pipelines; Upstash region maintenance or incident; corporate proxy returning an HTML error page; Vercel/Railway env vars not loaded before the seed run.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/bca211aa0091a405. Report an issue: GitHub.

Appendix: source

Thrown at scripts/seed-portwatch-port-activity.mjs:1012

  }
  if (commands.length === 0) return [];

  // Upstash /pipeline preserves command order but is explicitly non-atomic.
  // The canonical list and seed-meta are the publication pointers, so they
  // must commit in the same transaction as the per-country state they name.
  const resp = await fetchFn(`${credentials.url}/multi-exec`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${credentials.token}`,
      'Content-Type': 'application/json',
      'User-Agent': CHROME_UA,
    },
    body: JSON.stringify(commands),
    signal: AbortSignal.timeout(30_000),
  });
  if (!resp.ok) {
    const text = await resp.text().catch(() => '');
    throw new Error(`Redis transaction failed: HTTP ${resp.status} — ${text.slice(0, 200)}`);
  }
  const results = await resp.json();
  if (!Array.isArray(results) || results.length !== commands.length) {
    throw new Error(`Redis transaction failed: ${results?.error || 'invalid response'}`);
  }
  const failures = results.filter((result) => result?.error || result?.result === 'ERR');
  if (failures.length > 0) {
    throw new Error(`Redis transaction: ${failures.length}/${commands.length} commands failed`);
  }
  return results;
}

const CORRUPT_COUNTRY_CACHE = Symbol('corrupt country cache');

// MGET-style batch read via the Upstash REST /pipeline endpoint. Returns an
// array aligned with `keys` where each element is either the parsed JSON
// payload, explicit miss, or confirmed corrupt value. Transport/envelope errors
// remain fatal: only a validated upstream replacement may overwrite corruption.

View on GitHub (pinned to 7d06c8633d)