{"record":{"id":"ac1efaaab1f94d40","repo":"odysseus-dev/odysseus","slug":"data-detail-data-error-http-res-status","errorCode":null,"errorMessage":"data.detail || data.error || `HTTP ${res.status}`","messagePattern":"data\\.detail \\|\\| data\\.error \\|\\| `HTTP (.+?)`","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/cookbook-hwfit.js","lineNumber":2357,"sourceCode":"    if (!host) {\n      dot.className = 'cookbook-srv-status';\n      dot.title = 'Enter user@host to test';\n      setMsg('');\n      return;\n    }\n    dot.className = 'cookbook-srv-status testing';\n    dot.title = 'Testing SSH…';\n    setMsg('Testing SSH...');\n    const t0 = Date.now();\n    try {\n      const res = await fetch('/api/cookbook/test-ssh', {\n        method: 'POST', credentials: 'same-origin',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ host, ssh_port: port || undefined }),\n      });\n      const data = await res.json();\n      if (!res.ok) {\n        throw new Error(data.detail || data.error || `HTTP ${res.status}`);\n      }\n      const ms = Date.now() - t0;\n      const out = (data.stdout || '').trim();\n      if (data.exit_code === 0 && out.startsWith('ok')) {\n        dot.className = 'cookbook-srv-status ok';\n        dot.title = `Reachable · ${ms} ms · use Dependencies to check tmux/HF setup`;\n        setMsg(`Connected · ${ms} ms`, 'var(--green,#50fa7b)');\n      } else {\n        dot.className = 'cookbook-srv-status fail';\n        const err = (data.stderr || data.stdout || (data.exit_code == null ? 'no exit code' : `exit ${data.exit_code}`)).toString().trim().slice(0, 240);\n        dot.title = `SSH failed: ${err}`;\n        setMsg(`Failed · ${err}`, 'var(--red,#e06c75)');\n      }\n    } catch (e) {\n      dot.className = 'cookbook-srv-status fail';\n      dot.title = `Test failed: ${e.message || e}`;\n      setMsg(`Failed · ${e.message || e}`, 'var(--red,#e06c75)');\n    }","sourceCodeStart":2339,"sourceCodeEnd":2375,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/cookbook-hwfit.js#L2339-L2375","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst data = await res.json();\nif (!res.ok) {\n  throw new Error(data.detail || data.error || `HTTP ${res.status}`);\n}\n\n// after\nconst body = await res.text();\nlet data = {};\ntry { data = JSON.parse(body); } catch {}\nif (!res.ok) {\n  throw new Error(data.detail || data.error || `HTTP ${res.status} ${res.statusText}: ${body.slice(0, 160)}`);\n}","handlingStrategy":"validation","validationCode":"const m = host.match(/^(?<user>[^@\\s]+)@(?<host>[^@\\s]+)$/);\nif (!m) { setMsg('Host must be user@hostname', 'var(--red)'); return; }\nif (port && !/^\\d+$/.test(port)) { setMsg('Port must be numeric', 'var(--red)'); return; }","typeGuard":null,"tryCatchPattern":"try {\n  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 }) });\n  const body = await res.text();\n  let data = {};\n  try { data = JSON.parse(body); } catch { throw new Error(`HTTP ${res.status}: non-JSON response`); }\n  if (!res.ok) throw new Error(data.detail || data.error || `HTTP ${res.status}`);\n  // ... update dot/msg\n} catch (e) {\n  dot.className = 'cookbook-srv-status fail';\n  setMsg(`Test failed: ${e.message}`, 'var(--red)');\n}","preventionTips":["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)"],"tags":["fetch","http-error","ssh","cookbook","connectivity-check"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}