santifer/career-ops · error · Error

HTTP

Error message

HTTP ${resp.status}: ${t.slice(0, 120)}

What it means

Thrown inside callOpenRouter()'s request loop when the chat-completions endpoint returns a non-ok HTTP status. Unlike the models-list check, this includes the first 120 characters of the response body, which usually contains OpenRouter's JSON error explaining the model/status. Each failed model in the rotation is typically blacklisted and the next candidate tried.

Solutions

  1. Read the 120-char body snippet in the message — it names the actual problem (auth, credits, model id, moderation).
  2. 401/402: fix OPENROUTER_API_KEY or add credits; 429: the rotation should try the next model — if all fail, slow down.
  3. 404: the model was removed from OpenRouter; let loadFreeModels refresh the list on a fresh run.
  4. 400: check the request body — some free models reject params like temperature or system role.
  5. Check OpenRouter status page on persistent 5xx.

Example fix

// before
if (!resp.ok) {
  const t = await resp.text();
  throw new Error(`HTTP ${resp.status}: ${t.slice(0, 120)}`);
}
// after
if (!resp.ok) {
  const t = await resp.text();
  let detail = t.slice(0, 120);
  try { detail = JSON.parse(t)?.error?.message ?? detail; } catch {}
  const err = new Error(`HTTP ${resp.status}: ${detail}`);
  err.status = resp.status;
  throw err; // caller can branch on 429 vs 401 vs 404
}
Defensive patterns

Strategy: retry

Try / catch

try {
  result = await callOpenRouter(systemPrompt, userMessage);
} catch (err) {
  const m = /HTTP (\d{3})/.exec(err.message);
  const status = m ? Number(m[1]) : null;
  if (status === 429 || (status && status >= 500)) {
    await sleep(backoff(attempt++)); // transient — retry; rotation blacklists the model
  } else {
    throw err; // 401/402/400 need human action
  }
}

Prevention

When it happens

Trigger: POST to the completions endpoint returns 400 (malformed request/unsupported param for that model), 401/402 (key or credits), 404 (model id no longer exists), 429 (rate limited), or 5xx — with body text captured via resp.text().

Common situations: A free model was deprecated mid-session (404); per-model rate limits on free tier (429); provider upstream error surfaced as 502; moderation flagging the prompt (400).

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/88d829cdf9e72fb0. Report an issue: GitHub.

Appendix: source

Thrown at openrouter-runner.mjs:259

      max_tokens: MAX_TOKENS,
    });
    const ctrl = new AbortController();
    const timerId = setTimeout(() => ctrl.abort(), MODEL_TIMEOUT_MS);
    try {
      const resp = await fetch(OPENROUTER_API_URL, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${key}`,
          'Content-Type':  'application/json',
          'HTTP-Referer':  'https://github.com/career-ops-hq/career-ops',
          'X-Title':       'career-ops',
        },
        body,
        signal: ctrl.signal,
      });
      if (!resp.ok) {
        const t = await resp.text();
        throw new Error(`HTTP ${resp.status}: ${t.slice(0, 120)}`);
      }
      const data = await resp.json();
      if (data.error) throw new Error(data.error.message);
      const content = data.choices?.[0]?.message?.content ?? '';
      if (!content) throw new Error('Empty response');
      console.log('OK');
      const usage = normalizeOpenAIUsage(data.usage);
      return { content, usage };
    } catch (e) {
      if (e.name === 'AbortError') throw new Error(`Pinned model timed out after ${MODEL_TIMEOUT_MS / 1000}s`);
      throw e;
    } finally {
      clearTimeout(timerId);
    }
  }

  const models = await loadFreeModels();
  let lastError;

View on GitHub (pinned to aac998c7ed)