{"record":{"id":"00f8176354492482","repo":"ZhuLinsen/daily_stock_analysis","slug":"hermes-base-url-path-must-not-contain-encoded-segm","errorCode":null,"errorMessage":"Hermes BASE_URL path must not contain encoded segments","messagePattern":"Hermes BASE_URL path must not contain encoded segments","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/llm/hermes.py","lineNumber":182,"sourceCode":"    \"\"\"\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\"}:\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","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/llm/hermes.py#L164-L200","documentation":"Anti-evasion check in canonicalize_hermes_base_url: although the decoded path matched /v1, the RAW path must also be the plain literal (quote(decoded, safe='/') must equal the raw path modulo the trailing slash). This catches percent-encoded variants like '/%76%31' or '/v%31' that decode to /v1 — the bridge only accepts the unencoded literal, both to block URL-encoding tricks that could bypass path review and to keep the canonical URL deterministic.","triggerScenarios":"BASE_URL='http://127.0.0.1:8642/%76%31' or 'http://127.0.0.1:8642/v%31/' — anything whose raw path differs from its quoted-decoded form while still decoding to /v1. Plain '/v1' and '/v1/' never trigger this.","commonSituations":"URL-normalization tooling or proxies rewriting the path with encoding; hand-crafted URLs from security testing; configs passed through layers that percent-encode.","solutions":["Write the path literally: 'http://127.0.0.1:8642/v1' with no percent-encoding.","Find and disable whatever layer is re-encoding the URL (proxy, config manager) if you never typed encoded characters."],"exampleFix":"# before\nBASE_URL=http://127.0.0.1:8642/%76%31\n\n# after\nBASE_URL=http://127.0.0.1:8642/v1","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\nraw_path = urlparse(base_url).path or \"\"\nassert \"%\" not in raw_path and raw_path.rstrip(\"/\") == \"/v1\", \"use the literal /v1 path\"","typeGuard":"def is_plain_v1_path(value: str) -> bool:\n    raw = urlparse(value).path or \"\"\n    return \"%\" not in raw and raw.rstrip(\"/\") == \"/v1\"","tryCatchPattern":"try:\n    url = canonicalize_hermes_base_url(cfg.base_url)\nexcept ValueError as exc:\n    raise ConfigError(str(exc)) from exc","preventionTips":["Never percent-encode the path; type /v1 literally","Check proxies/config managers that re-encode URLs","Treat encoded-path configs as a security smell, not a workaround"],"tags":["hermes","llm","config","url-validation","security"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}