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
- Check whether res.json() succeeded — if the endpoint returns HTML on error, parse text first and JSON-parse defensively
- Validate host is 'user@hostname' and port is numeric before sending
- Confirm the server process has an ssh client available (which ssh on the host)
- 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
- Parse the body as text first, then JSON.parse defensively — res.json() can throw on HTML error pages
- Validate user@host and numeric port client-side before the request
- Distinguish transport errors (this throw) from SSH failures (exit_code !== 0 path)
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
- HTTP ${res.status}
- HTTP ${res.status} ${res.statusText}${msg ? `: ${msg}` : ''}
- data.error || 'Failed to generate SSH key'
- HTTP ${res.status}
- detail || ('HTTP ' + res.status)
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/ac1efaaab1f94d40.
Report an issue: GitHub.