jackwener/OpenCLI · error · CommandExecutionError

Lobsters API HTTP ${res.status} for story ${shortId}

Error message

Lobsters API HTTP ${res.status} for story ${shortId}

What it means

Thrown by fetchStory in clis/lobsters/read.js:26 when the lobste.rs JSON endpoint returns a non-OK status other than 404. 404 is converted to EmptyResultError; anything else (5xx, 403 rate-limited, network-edge errors surfaced as status) becomes this generic CommandExecutionError wrapping the HTTP status and the story short id. It signals the request reached the Lobsters API but the API rejected or failed to serve it.

Source

Thrown at clis/lobsters/read.js:26

 * just need one HTTP call, then build a children map and DFS.
 *
 * Output rows:
 *   - first row is the story itself (`type=POST`)
 *   - each subsequent row is a comment, indented by depth (`L0`, `L1`, …)
 *   - `[+N more replies]` summary rows whenever depth/limit cuts in
 */
import { cli, Strategy } from '@jackwener/opencli/registry';
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';

const LOBSTERS_STORY_BASE = 'https://lobste.rs/s';

async function fetchStory(shortId) {
    const res = await fetch(`${LOBSTERS_STORY_BASE}/${shortId}.json`);
    if (res.status === 404) {
        throw new EmptyResultError(`lobsters/${shortId}`, 'Story not found');
    }
    if (!res.ok) {
        throw new CommandExecutionError(`Lobsters API HTTP ${res.status} for story ${shortId}`, 'Check the short id');
    }
    return res.json();
}

function requirePositiveInt(value, label) {
    if (!Number.isInteger(value) || value <= 0) {
        throw new ArgumentError(`${label} must be a positive integer`);
    }
    return value;
}

function requireMinInt(value, min, label) {
    if (!Number.isInteger(value) || value < min) {
        throw new ArgumentError(`${label} must be an integer >= ${min}`);
    }
    return value;
}

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Check https://lobste.rs is reachable in a browser and inspect the actual HTTP status from the message
  2. Retry after a delay if the status is 429/5xx — Lobsters rate-limits and has outages
  3. Verify the short id is a valid lowercase-alphanumeric id (regex ^[a-z0-9]+$ passes before fetch)
  4. Check for proxy/VPN/firewall interference with lobste.rs requests
  5. If persistent, capture res body/status and report to Lobsters or the CLI maintainers

Example fix

// before
const story = await fetchStory(shortId);
// after
try {
  const story = await fetchStory(shortId);
} catch (e) {
  if (/HTTP 429|HTTP 5\d\d/.test(e.message)) {
    await new Promise(r => setTimeout(r, 5000));
    return fetchStoryWithRetry(shortId);
  }
  throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

const shortId = String(id || '').trim();
if (!/^[a-z0-9]+$/.test(shortId)) throw new Error(`Invalid short id: ${id}`);

Type guard

function isValidShortId(id) {
  return typeof id === 'string' && /^[a-z0-9]+$/.test(id.trim());
}

Try / catch

try {
  const story = await fetchStory(id);
} catch (e) {
  if (/HTTP (429|5\d\d)/.test(e.message)) {
    await sleep(5000);
    return fetchStory(id); // bounded retries
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `lobsters read <short_id>` where the GET https://lobste.rs/s/<short_id>.json returns e.g. 500/502/503 (Lobsters outage or Cloudflare issue) or 403/429 (rate limiting, blocked IP). Only statuses other than 404 produce this.

Common situations: Lobsters is down or under maintenance; a corporate proxy/CDN blocks lobste.rs; short-id typos that accidentally hit an edge route returning 403/410 instead of 404; aggressive scripted polling triggering 429 rate limits.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/4e62188f9d832aa2. Report an issue: GitHub.