odysseus-dev/odysseus · error · Error

data.detail || data.error || `HTTP ${res.status}`

Error message

data.detail || data.error || `HTTP ${res.status}`

What it means

Error thrown after POST /api/cookbook/test-ssh answers non-OK while probing a cookbook server's SSH reachability. The JSON body is parsed first (res.json() runs before the ok check), and detail/error/status are merged into the message. The surrounding UI turns the status dot red/green based on data, so this throw only fires on transport-level or server-side failures, not on mere SSH failure.

Source

Thrown at static/js/cookbook-hwfit.js:2357

    if (!host) {
      dot.className = 'cookbook-srv-status';
      dot.title = 'Enter user@host to test';
      setMsg('');
      return;
    }
    dot.className = 'cookbook-srv-status testing';
    dot.title = 'Testing SSH…';
    setMsg('Testing SSH...');
    const t0 = Date.now();
    try {
      const res = await fetch('/api/cookbook/test-ssh', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ host, ssh_port: port || undefined }),
      });
      const data = await res.json();
      if (!res.ok) {
        throw new Error(data.detail || data.error || `HTTP ${res.status}`);
      }
      const ms = Date.now() - t0;
      const out = (data.stdout || '').trim();
      if (data.exit_code === 0 && out.startsWith('ok')) {
        dot.className = 'cookbook-srv-status ok';
        dot.title = `Reachable · ${ms} ms · use Dependencies to check tmux/HF setup`;
        setMsg(`Connected · ${ms} ms`, 'var(--green,#50fa7b)');
      } else {
        dot.className = 'cookbook-srv-status fail';
        const err = (data.stderr || data.stdout || (data.exit_code == null ? 'no exit code' : `exit ${data.exit_code}`)).toString().trim().slice(0, 240);
        dot.title = `SSH failed: ${err}`;
        setMsg(`Failed · ${err}`, 'var(--red,#e06c75)');
      }
    } catch (e) {
      dot.className = 'cookbook-srv-status fail';
      dot.title = `Test failed: ${e.message || e}`;
      setMsg(`Failed · ${e.message || e}`, 'var(--red,#e06c75)');
    }

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Check whether res.json() succeeded — if the endpoint returns HTML on error, parse text first and JSON-parse defensively
  2. Validate host is 'user@hostname' and port is numeric before sending
  3. Confirm the server process has an ssh client available (which ssh on the host)
  4. Look at backend logs for the exception behind the non-OK status

Example fix

// before
const data = await res.json();
if (!res.ok) {
  throw new Error(data.detail || data.error || `HTTP ${res.status}`);
}

// after
const body = await res.text();
let data = {};
try { data = JSON.parse(body); } catch {}
if (!res.ok) {
  throw new Error(data.detail || data.error || `HTTP ${res.status} ${res.statusText}: ${body.slice(0, 160)}`);
}
Defensive patterns

Strategy: validation

Validate before calling

const m = host.match(/^(?<user>[^@\s]+)@(?<host>[^@\s]+)$/);
if (!m) { setMsg('Host must be user@hostname', 'var(--red)'); return; }
if (port && !/^\d+$/.test(port)) { setMsg('Port must be numeric', 'var(--red)'); return; }

Try / catch

try {
  const res = await fetch('/api/cookbook/test-ssh', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ host, ssh_port: port || undefined }) });
  const body = await res.text();
  let data = {};
  try { data = JSON.parse(body); } catch { throw new Error(`HTTP ${res.status}: non-JSON response`); }
  if (!res.ok) throw new Error(data.detail || data.error || `HTTP ${res.status}`);
  // ... update dot/msg
} catch (e) {
  dot.className = 'cookbook-srv-status fail';
  setMsg(`Test failed: ${e.message}`, 'var(--red)');
}

Prevention

When it happens

Trigger: POST /api/cookbook/test-ssh returns 500 (ssh client crashed server-side), 422 (host without user@ format, non-numeric port), or the body is not JSON so res.json() itself throws before this line; auth middleware returning 401.

Common situations: Host field missing the user@ portion; port field contains junk; backend lacks an ssh binary in PATH; reverse proxy intercepts with an HTML error page making res.json() reject first; session cookie expired so a 401 HTML page is returned.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/ac1efaaab1f94d40. Report an issue: GitHub.