{"record":{"id":"6f1d0ede90b71cf6","repo":"infiniflow/ragflow","slug":"keenable-api-url-must-be-an-https-url-with-a-ho","errorCode":null,"errorMessage":"KEENABLE_API_URL must be an https:// URL with a host, got {base!r}","messagePattern":"KEENABLE_API_URL must be an https:// URL with a host, got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent/tools/keenable.py","lineNumber":38,"sourceCode":"from urllib.parse import urlsplit\n\nimport requests\n\nfrom agent.tools.base import ToolBase, ToolMeta, ToolParamBase\nfrom common.connection_utils import timeout\n\n\ndef _base_url() -> str:\n    \"\"\"Resolve the Keenable API base URL from ``KEENABLE_API_URL`` (HTTPS enforced).\"\"\"\n    base = (os.environ.get(\"KEENABLE_API_URL\") or \"https://api.keenable.ai\").rstrip(\"/\")\n    parsed = urlsplit(base)\n    if parsed.hostname:\n        if parsed.scheme == \"https\":\n            return base\n        # Permit plain http only against a loopback host (local dev).\n        if parsed.scheme == \"http\" and parsed.hostname in {\"localhost\", \"127.0.0.1\", \"::1\"}:\n            return base\n    raise ValueError(f\"KEENABLE_API_URL must be an https:// URL with a host, got {base!r}\")\n\n\ndef _request(method: str, public_path: str, keyed_path: str, api_key: str, *, params=None, json=None, timeout_s: int = 30):\n    \"\"\"Call the keyed endpoint with X-API-Key when a key is set, else the keyless public one.\"\"\"\n    api_key = (api_key or \"\").strip()\n    headers = {\n        \"User-Agent\": \"keenable-ragflow\",\n        # Attribution header the Keenable backend segments traffic by.\n        \"X-Keenable-Title\": \"RAGFlow\",\n    }\n    if api_key:\n        path = keyed_path\n        headers[\"X-API-Key\"] = api_key\n    else:\n        path = public_path\n    resp = requests.request(method, f\"{_base_url()}{path}\", headers=headers, params=params, json=json, timeout=timeout_s)\n    resp.raise_for_status()\n    return resp.json()","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/agent/tools/keenable.py#L20-L56","documentation":"ValueError raised by _base_url() in the Keenable tool when the KEENABLE_API_URL environment variable is not a usable HTTPS URL. The helper strips trailing '/', parses the URL, and only accepts https:// any host, or http:// strictly for localhost/127.0.0.1/::1 (local dev). Anything else — http:// to a remote host, missing scheme, no hostname — raises.","triggerScenarios":"Setting KEENABLE_API_URL=http://api.keenable.ai (plain http off-loopback), KEENABLE_API_URL=api.keenable.ai (no scheme, parsed.hostname is None), an empty string after rstrip, or a URL like 'https://<spaces>' that fails host parsing.","commonSituations":"Operators copying an internal http:// endpoint URL from staging into production env config; docker-compose env files missing the scheme; typos like 'https:/api.keenable.ai' (single slash yields no netloc); proxies terminating TLS prompting someone to 'downgrade' to http.","solutions":["Set KEENABLE_API_URL to a full https URL with a host, e.g. https://api.keenable.ai.","If you truly need plain http for local testing, the host must be exactly localhost, 127.0.0.1, or ::1.","If the URL sits behind an http-only proxy, put a TLS terminator (nginx/caddy) in front and point the variable at the https endpoint.","Unset the variable entirely to fall back to the default https://api.keenable.ai."],"exampleFix":"# before\nexport KEENABLE_API_URL=http://api.keenable.ai   # ValueError\n\n# after\nexport KEENABLE_API_URL=https://api.keenable.ai\n# local dev only:\nexport KEENABLE_API_URL=http://127.0.0.1:8080","handlingStrategy":"validation","validationCode":"from urllib.parse import urlsplit\ndef valid_keenable_url(v: str) -> bool:\n    p = urlsplit((v or \"\").rstrip(\"/\"))\n    return (p.scheme == \"https\" and bool(p.hostname)) or \\\n           (p.scheme == \"http\" and p.hostname in {\"localhost\", \"127.0.0.1\", \"::1\"})","typeGuard":null,"tryCatchPattern":"try:\n        keenable_search._invoke(query=q)\n    except ValueError as e:\n        if \"KEENABLE_API_URL\" in str(e):\n            fix_environment(); restart_service()\n        raise","preventionTips":["Set KEENABLE_API_URL once in deployment config with a full https:// URL and a smoke-test at deploy time.","Add a config lint step (CI or bootstrap) that validates the env var with urlsplit before the agent runs.","Leave the variable unset to use the default https://api.keenable.ai."],"tags":["configuration","environment","https","ssrf-protection","keenable"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}