{"record":{"id":"e5a5dc77dcedcefb","repo":"odysseus-dev/odysseus","slug":"detail","errorCode":null,"errorMessage":"detail","messagePattern":"detail","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/webhook/webhook_routes.py","lineNumber":292,"sourceCode":"            # chat-scoped token.\n            _sess_owner = getattr(sess, \"owner\", None)\n            if not _caller_owns_session(_sess_owner, _tok_user):\n                raise HTTPException(404, \"Session not found\")\n\n        # --- Case 2: Direct API key + model (no pre-configured endpoint needed) ---\n        if not sess and body.api_key:\n            api_key = body.api_key.strip()\n            model = body.model or \"deepseek-chat\"\n\n            # Validate only token-supplied direct base_url; auto-resolved known-provider\n            # URLs are not subject to extra local/LAN blocking beyond existing provider logic.\n            direct_base_url = body.base_url.strip().rstrip(\"/\") if body.base_url else None\n            if direct_base_url:\n                try:\n                    base_url = validate_public_http_url(direct_base_url)\n                except ValueError as e:\n                    detail = str(e).replace(\"URL\", \"base_url\", 1)\n                    raise HTTPException(400, detail)\n            else:\n                base_url = _resolve_base_url(model, body.provider)\n            if not base_url:\n                raise HTTPException(400,\n                    \"Could not auto-detect provider. Pass base_url (e.g. 'https://api.deepseek.com/v1') \"\n                    \"or provider ('deepseek', 'openai', 'groq', etc.)\")\n            base_url = normalize_base(base_url)\n            endpoint_url = build_chat_url(base_url)\n\n            if not session_manager:\n                raise HTTPException(500, \"Session manager not available\")\n\n            sid = str(uuid.uuid4())\n            sess = session_manager.create_session(\n                session_id=sid, name=\"API Chat\", endpoint_url=endpoint_url,\n                model=model, owner=token_owner,\n            )\n            sess.headers = build_headers(api_key, base_url)","sourceCodeStart":274,"sourceCodeEnd":310,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/webhook/webhook_routes.py#L274-L310","documentation":"HTTP 400 from POST /v1/chat Case 2 (direct api_key): the caller-supplied base_url failed validate_public_http_url() in src/url_security.py. The detail is the ValueError message with the first 'URL' replaced by 'base_url': either 'base_url is too long' or 'base_url must point to a public HTTP(S) endpoint'. The check enforces http(s) scheme, resolvable DNS, and no private/blocked IPs — fail-closed on DNS errors.","triggerScenarios":"Passing base_url=http://192.168.1.50:11434/v1 (local Ollama), base_url=http://localhost:8000, an ftp:// URL, a >2048-char URL, or a hostname that does not resolve from the server.","commonSituations":"Trying to use a self-hosted LAN model server (vLLM/Ollama/LM Studio) through the token-facing chat API — blocked because API-token callers are untrusted and private targets are SSRF surfaces; typos in domains; split-horizon DNS where the name resolves internally only.","solutions":["Point base_url at a publicly reachable https endpoint (expose the model server via a tunnel or host it publicly)","If the model server is trusted infra, have an admin configure it as a ModelEndpoint (Case 3 path) instead of passing base_url per-request","Verify the hostname resolves publicly from the server before using it"],"exampleFix":"// before\n{\"message\":\"hi\",\"api_key\":\"sk-...\",\"model\":\"llama3\",\"base_url\":\"http://192.168.1.50:11434/v1\"}\n// -> 400 \"base_url must point to a public HTTP(S) endpoint\"\n\n// after: omit base_url and use a known provider, or a public endpoint\n{\"message\":\"hi\",\"api_key\":\"sk-...\",\"model\":\"deepseek-chat\"}","handlingStrategy":"validation","validationCode":"from src.url_security import validate_public_http_url\ntry:\n    base = validate_public_http_url(candidate_base_url)\nexcept ValueError as e:\n    inform_user(str(e).replace(\"URL\", \"base_url\", 1))  # do not send","typeGuard":null,"tryCatchPattern":"if resp.status_code == 400 and 'base_url' in detail:\n    # private/unresolvable base_url: switch to public endpoint or drop base_url","preventionTips":["Never pass LAN/localhost base_urls to /v1/chat; expose models publicly or use admin-configured endpoints","Check the hostname resolves publicly from the server before configuring"],"tags":["ssrf","validation","base-url","chat","http-400"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}