{"record":{"id":"d24f0910f2e30336","repo":"ZhuLinsen/daily_stock_analysis","slug":"hermes-base-url-must-point-to-127-0-0-1-localhost","errorCode":null,"errorMessage":"Hermes BASE_URL must point to 127.0.0.1, localhost, or [::1]","messagePattern":"Hermes BASE_URL must point to 127\\.0\\.0\\.1, localhost, or \\[::1\\]","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/llm/hermes.py","lineNumber":188,"sourceCode":"    if not parsed.netloc or not parsed.hostname:\n        raise ValueError(\"Hermes BASE_URL must include a loopback host\")\n    if parsed.username or parsed.password:\n        raise ValueError(\"Hermes BASE_URL must not include userinfo\")\n    if parsed.params or parsed.query or parsed.fragment:\n        raise ValueError(\"Hermes BASE_URL must not include params, query, or fragment\")\n\n    raw_path = parsed.path or \"\"\n    decoded_path = unquote(raw_path)\n    if decoded_path not in {\"/v1\", \"/v1/\"}:\n        raise ValueError(\"Hermes BASE_URL path must be /v1\")\n    if quote(decoded_path, safe=\"/\") != raw_path.rstrip(\"/\") and raw_path not in {\"/v1\", \"/v1/\"}:\n        raise ValueError(\"Hermes BASE_URL path must not contain encoded segments\")\n\n    hostname = parsed.hostname.strip().lower()\n    if hostname == \"localhost\":\n        hostname = \"127.0.0.1\"\n    elif hostname not in {\"127.0.0.1\", \"::1\"}:\n        raise ValueError(\"Hermes BASE_URL must point to 127.0.0.1, localhost, or [::1]\")\n\n    try:\n        port = parsed.port\n    except ValueError as exc:\n        raise ValueError(\"Hermes BASE_URL contains an invalid port\") from exc\n\n    netloc = f\"[{hostname}]\" if \":\" in hostname else hostname\n    if port is not None:\n        netloc = f\"{netloc}:{port}\"\n    return urlunparse(parsed._replace(netloc=netloc, path=\"/v1\", params=\"\", query=\"\", fragment=\"\"))\n\n\ndef canonicalize_hermes_model_ref(raw_model: str) -> HermesModelRef:\n    \"\"\"Return the canonical DSA route and LiteLLM wire model for Hermes.\n\n    Hermes is OpenAI-compatible over local HTTP, so both route identity and\n    outbound wire model use LiteLLM's openai/ namespace.  The display label is\n    only UI metadata and must not be used for routing or provider detection.","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/llm/hermes.py#L170-L206","documentation":"Raised while canonicalizing the Hermes LLM base URL. The validation function only allows loopback hosts (127.0.0.1, localhost, [::1]) because Hermes is a local, OpenAI-compatible HTTP endpoint; any remote hostname or IP is rejected before the client is built. The error surfaces as a ValueError from URL parsing at config-load time.","triggerScenarios":"Calling the Hermes URL canonicalizer with a BASE_URL whose hostname is anything other than 127.0.0.1, localhost, or ::1 — e.g. http://0.0.0.0:8080/v1, http://192.168.1.5:8080/v1, https://api.example.com/v1, or a bare host like http://hermes:8080/v1. Note that 0.0.0.0 is explicitly NOT in the allowlist even though it is often used for local listeners.","commonSituations":"Pointing Hermes at a machine reachable on the LAN instead of localhost; using 0.0.0.0 (the bind address) as the connect address; copying a docker-compose service name into BASE_URL; leaving a placeholder cloud URL in .env after switching from a remote provider to the local Hermes backend.","solutions":["Set HERMES_BASE_URL (or the equivalent env/config key) to a loopback URL with the /v1 path, e.g. http://127.0.0.1:8080/v1","If Hermes runs in Docker or on another host, port-forward it to localhost (e.g. docker -p 127.0.0.1:8080:8080 or ssh -L) so the URL can stay loopback","Replace 0.0.0.0 with 127.0.0.1 in the URL — 0.0.0.0 is a bind address, not a valid connect target under this policy","Verify the path component is exactly /v1 or /v1/ and contains no percent-encoded segments, since those raise adjacent validation errors"],"exampleFix":"# before\nHERMES_BASE_URL=http://0.0.0.0:8080/v1\n\n# after\nHERMES_BASE_URL=http://127.0.0.1:8080/v1","handlingStrategy":"validation","validationCode":"from urllib.parse import urlsplit\n\ndef is_valid_hermes_base_url(url: str) -> bool:\n    try:\n        parsed = urlsplit(url)\n    except ValueError:\n        return False\n    host = (parsed.hostname or \"\").strip().lower()\n    if host not in {\"127.0.0.1\", \"::1\", \"localhost\"}:\n        return False\n    from urllib.parse import unquote\n    path = unquote(parsed.path or \"\")\n    return path in {\"/v1\", \"/v1/\"}","typeGuard":null,"tryCatchPattern":"try:\n    url = canonicalize_hermes_base_url(raw)\nexcept ValueError as exc:\n    raise ConfigError(f\"HERMES_BASE_URL invalid: {exc}\") from exc","preventionTips":["Validate HERMES_BASE_URL once at startup and fail fast with the allowed format in the message","Never use 0.0.0.0 as a connect address; bind wide, connect to 127.0.0.1","Keep a single source of truth for the URL in env config, not hardcoded strings"],"tags":["config","llm","validation","url"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}