{"record":{"id":"30402bb02d18e4f2","repo":"odysseus-dev/odysseus","slug":"host-is-required","errorCode":null,"errorMessage":"host is required","messagePattern":"host is required","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/cookbook_routes.py","lineNumber":2843,"sourceCode":"        except Exception:\n            pass\n\n        return {\"ok\": True, \"session_id\": session_id, \"remote\": remote or \"local\",\n                \"endpoint_id\": endpoint_id}\n\n    # ── Server setup (install deps on remote) ──\n\n    class SetupRequest(BaseModel):\n        host: str\n        ssh_port: str | None = None\n\n    @router.post(\"/api/cookbook/setup\")\n    async def server_setup(request: Request, req: SetupRequest):\n        \"\"\"Install required dependencies on a remote server via SSH.\"\"\"\n        require_admin(request)\n        host = validate_remote_host(req.host)\n        if not host:\n            raise HTTPException(400, \"host is required\")\n        port = req.ssh_port\n        port = validate_ssh_port(port)\n        pf = f\"-p {port} \" if port and port != \"22\" else \"\"\n\n        # Detect platform: Windows first (echo %OS% → Windows_NT), then Termux, then Linux\n        detect_cmd = f'ssh {pf}{host} \"echo %OS%\"'\n        platform = \"linux\"\n        try:\n            proc = await asyncio.create_subprocess_shell(\n                detect_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE\n            )\n            stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=10)\n            out = stdout.decode().strip()\n            if \"Windows_NT\" in out:\n                platform = \"windows\"\n            else:\n                # Check for Termux\n                detect_cmd2 = f\"ssh {pf}{host} 'test -d /data/data/com.termux && echo termux || echo linux'\"","sourceCodeStart":2825,"sourceCodeEnd":2861,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/cookbook_routes.py#L2825-L2861","documentation":"Raised (HTTP 400) by POST /api/cookbook/setup when the SetupRequest body's host field normalizes to empty. validate_remote_host() returns None for empty/None input (it only raises for malformed hosts), and the route then requires a non-empty host because dependency installation only works over SSH against a named remote server. Admin-gated via require_admin.","triggerScenarios":"POST /api/cookbook/setup with host omitted, host: \"\", or host: null in the JSON body. Pydantic's SetupRequest declares host: str (required), so omitting it entirely fails Pydantic validation with 422 instead; this 400 appears when host is present but empty string.","commonSituations":"Frontend sends an empty host field because the user never filled in the remote-server input; a form-reset bug leaves host=''; whitespace-only host slips past client-side checks.","solutions":["Send a valid host (or user@host) in the SetupRequest body, e.g. {\"host\": \"user@10.0.0.5\", \"ssh_port\": \"22\"}.","Add a client-side required-field check on the host input before submitting the setup form.","Note the accepted format: validate_remote_host only allows 'host' or 'user@host' with no spaces or SSH option syntax — values like 'ssh -i key host' get a different 400 ('Invalid remote_host')."],"exampleFix":"// before\nawait post('/api/cookbook/setup', { host: '', ssh_port: '22' }); // 400 host is required\n\n// after\nawait post('/api/cookbook/setup', { host: 'user@gpu-box', ssh_port: '22' });","handlingStrategy":"validation","validationCode":"const HOST_RE = /^(?:[A-Za-z0-9][A-Za-z0-9._-]*@)?[A-Za-z0-9][A-Za-z0-9._-]*$/;\nfunction validateSetupBody(body) {\n  if (!body?.host || !HOST_RE.test(body.host)) throw new Error('host is required and must be host or user@host');\n  return body;\n}","typeGuard":"function isSetupRequest(b: unknown): b is { host: string; ssh_port?: string } {\n  return typeof b === 'object' && b !== null && typeof (b as any).host === 'string' && (b as any).host.length > 0;\n}","tryCatchPattern":"try { await post('/api/cookbook/setup', body); } catch (e) { if (e.status === 400 && e.detail === 'host is required') { focusHostInput(); return; } throw e; }","preventionTips":["Make the host input required in the form UI before enabling submit.","Reuse the same host regex client-side as routes/_validators.py to catch both empty and malformed hosts early."],"tags":["cookbook","setup","ssh","validation","admin"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}