{"record":{"id":"830abe3700801896","repo":"BerriAI/litellm","slug":"compresr-guardrail-api-base-must-be-http-or-https","errorCode":null,"errorMessage":"Compresr guardrail api_base must be http or https, got scheme={parsed.scheme!r}","messagePattern":"Compresr guardrail api_base must be http or https, got scheme=(.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/proxy/guardrails/guardrail_hooks/compresr/compresr.py","lineNumber":134,"sourceCode":"            return None\n    if isinstance(addr, ipaddress.IPv6Address) and addr.ipv4_mapped is not None:\n        return addr.ipv4_mapped\n    return addr\n\n\ndef _validate_api_base(url: str) -> str:\n    \"\"\"Return ``url`` if it passes basic outbound-target checks, else raise.\n\n    Best-effort defense in depth for a mis/maliciously-configured ``api_base``:\n    rejects non-http(s) schemes and cloud-metadata IPs/hosts (incl. alternate IP\n    encodings); private ranges are allowed for on-prem deployments. NOT a complete\n    SSRF control — no DNS resolution, and the shared client follows redirects and\n    re-resolves DNS (TOCTOU / rebinding); ``api_base`` is trusted operator config,\n    so this is an accepted limitation.\n    \"\"\"\n    parsed: Final = urlparse(url)\n    if parsed.scheme not in (\"http\", \"https\"):\n        raise ValueError(f\"Compresr guardrail api_base must be http or https, got scheme={parsed.scheme!r}\")\n    host: Final = (parsed.hostname or \"\").lower()\n    if not host:\n        raise ValueError(\"Compresr guardrail api_base has no host\")\n    ip_literal: Final = _parse_ip_literal(host)\n    if host in _BLOCKED_METADATA_HOSTS or (ip_literal is not None and ip_literal in _BLOCKED_METADATA_IPS):\n        raise ValueError(f\"Compresr guardrail api_base {host!r} is a blocked cloud-metadata host\")\n    return url\n\n\ndef _is_str_object_dict(value: object) -> TypeGuard[dict[str, object]]:  # guard-ok: isinstance narrows correctly; predicate is trivially correct  # fmt: skip\n    return isinstance(value, dict)\n\n\ndef _is_object_list(value: object) -> TypeGuard[list[object]]:  # guard-ok: isinstance narrows correctly; predicate is trivially correct  # fmt: skip\n    return isinstance(value, list)\n\n\ndef _replace_text_in_content(content: object, new_text: str) -> object:","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/guardrails/guardrail_hooks/compresr/compresr.py#L116-L152","documentation":"ValueError from _validate_api_base when constructing the Compresr guardrail: the configured/derived api_base has a URL scheme other than http or https (the {parsed.scheme!r} of the actual scheme is shown, e.g. 'file', 'ws', 'gcpmetadata'). This is the first check in a defense-in-depth SSRF review of the outbound target — the guardrail refuses to initialize against non-HTTP targets. api_base is operator config, so this fires at startup when the config value is malformed.","triggerScenarios":"Setting api_base: file:///etc/passwd, ws://..., ftp://..., or a schemeless value that urlparse interprets oddly (e.g., 'localhost:8080' where 'localhost' becomes the scheme); passing a Compresr API base copied from a websocket URL or with a typo like 'httpx://'.","commonSituations":"YAML values without the http:// prefix (localhost:9000 parsed as scheme=localhost); copying ws:// endpoints from Compresr streaming docs; env var COMPRESR_API_BASE containing a trailing fragment or wrong protocol.","solutions":["Fix api_base to a full http(s) URL, e.g., https://api.compresr.example or http://localhost:8080 (note the // — bare host:port is parsed as a scheme).","Verify with urlparse in a REPL: from urllib.parse import urlparse; urlparse(url).scheme must be 'http' or 'https'.","If using COMPRESR_API_BASE, correct the env var and restart the proxy."],"exampleFix":"# before (parsed as scheme='localhost')\nlitellm_params:\n  guardrail: compresr\n  api_base: localhost:8080\n\n# after\nlitellm_params:\n  guardrail: compresr\n  api_base: http://localhost:8080","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\nREQUIRED_SCHEMES = (\"http\", \"https\")\n\ndef validate_compresr_api_base(url: str) -> str:\n    scheme = urlparse(url).scheme.lower()\n    if scheme not in REQUIRED_SCHEMES:\n        raise ValueError(f\"api_base must be http/https, got scheme={scheme!r} — include 'http://' for bare host:port\")\n    return url\n\nvalidate_compresr_api_base(cfg_guardrail[\"api_base\"])  # before proxy start","typeGuard":"from urllib.parse import urlparse\nfrom typing import TypeGuard\ndef is_http_url(value: object) -> TypeGuard[str]:\n    if not isinstance(value, str):\n        return False\n    p = urlparse(value)\n    return p.scheme in (\"http\", \"https\") and bool(p.hostname)","tryCatchPattern":null,"preventionTips":["Never write host:port without a scheme — urlparse reads the host as the scheme.","Add a config-lint step that urlparse-checks every guardrail api_base.","When copying endpoints from docs, confirm the protocol prefix matches what the client library expects (httpx needs http/https)."],"tags":["litellm","guardrails","compresr","ssrf","url-validation","configuration"],"backgroundTag":"config-validation-failed","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}