{"record":{"id":"ed6d33ba01ffc9fb","repo":"ZhuLinsen/daily_stock_analysis","slug":"hermes-base-url-must-use-http-or-https","errorCode":null,"errorMessage":"Hermes BASE_URL must use http or https","messagePattern":"Hermes BASE_URL must use http or https","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/llm/hermes.py","lineNumber":169,"sourceCode":"\ndef canonicalize_hermes_protocol(protocol: str) -> str:\n    normalized = (protocol or HERMES_DEFAULT_PROTOCOL).strip().lower() or HERMES_DEFAULT_PROTOCOL\n    if normalized != HERMES_DEFAULT_PROTOCOL:\n        raise ValueError(\"Hermes only supports PROTOCOL=openai\")\n    return HERMES_DEFAULT_PROTOCOL\n\n\ndef canonicalize_hermes_base_url(base_url: str) -> str:\n    \"\"\"Return canonical Hermes base URL or raise ValueError.\n\n    Allowed forms are loopback HTTP(S) URLs whose path is exactly /v1 or /v1/.\n    localhost is canonicalized to 127.0.0.1 to avoid DNS/hosts ambiguity.\n    \"\"\"\n\n    raw = (base_url or HERMES_DEFAULT_BASE_URL).strip() or HERMES_DEFAULT_BASE_URL\n    parsed = urlparse(raw)\n    if parsed.scheme.lower() not in {\"http\", \"https\"}:\n        raise ValueError(\"Hermes BASE_URL must use http or https\")\n    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\"}:","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/llm/hermes.py#L151-L187","documentation":"First check in canonicalize_hermes_base_url (src/llm/hermes.py): the Hermes BASE_URL must parse with scheme http or https. Because Hermes is a loopback-only bridge (see later host checks), other schemes are never valid — ftp/file/tcp schemes, or scheme-less strings like '127.0.0.1:8642/v1', are rejected here before host/path validation runs.","triggerScenarios":"BASE_URL set without a scheme ('127.0.0.1:8642/v1' — urlparse treats '127.0.0.1' as the scheme), or with a non-HTTP scheme. Empty input falls back to HERMES_DEFAULT_BASE_URL ('http://127.0.0.1:8642/v1') and does NOT trigger this.","commonSituations":"Omitting http:// when configuring the URL (most common); pasting a ws:// or grpc:// endpoint; trailing garbage that breaks URL parsing.","solutions":["Include the scheme: http://127.0.0.1:8642/v1 (or https on loopback).","Leave BASE_URL unset to use the default loopback URL if that is the deployment."],"exampleFix":"# before\nBASE_URL=127.0.0.1:8642/v1\n\n# after\nBASE_URL=http://127.0.0.1:8642/v1","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\nparsed = urlparse(base_url)\nassert parsed.scheme.lower() in {\"http\", \"https\"}, \"include http(s):// in BASE_URL\"","typeGuard":"def is_http_url(value: str) -> bool:\n    return urlparse(value or \"\").scheme.lower() in {\"http\", \"https\"}","tryCatchPattern":"try:\n    url = canonicalize_hermes_base_url(cfg.base_url)\nexcept ValueError as exc:\n    raise ConfigError(f\"HERMES BASE_URL invalid: {exc}\") from exc","preventionTips":["Always write the http:// or https:// scheme explicitly","Leave BASE_URL unset to use the default","Smoke-test config parsing at startup"],"tags":["hermes","llm","config","url-validation"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}