JuliusBrussee/caveman · error · ValueError

must be an absolute http(s) URL

Error message

{name} must be an absolute http(s) URL

What it means

`_normalized_service_url` parses the URL with `urlsplit`; if parsing raises (e.g. an invalid port like `:abc`) or the parsed result is not an http/https absolute URL with a hostname, it raises this ValueError. The library only accepts absolute `http://` or `https://` URLs for service endpoints.

Solutions

  1. Prefix the URL with `https://` (or `http://` for local dev) if the scheme is missing.
  2. Use a full absolute URL including scheme and host, e.g. `https://cave.example.com`.
  3. Check for a malformed port (`host:port` must be numeric) or remove invalid characters.
  4. Ensure the value is non-empty before constructing.

Example fix

// before
cave = Cave(api_key=key, service_url="localhost:8080")
// after
cave = Cave(api_key=key, service_url="http://localhost:8080")
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlsplit
def is_absolute_http_url(v: str) -> bool:
    try:
        p = urlsplit(v)
    except ValueError:
        return False
    return p.scheme in ("http", "https") and bool(p.hostname)

Try / catch

try:
    cave = Cave(api_key=key, service_url=url)
except ValueError:
    url = "https://" + url.lstrip("/")
    cave = Cave(api_key=key, service_url=url)

Prevention

When it happens

Trigger: Passing a relative path (`/api`), a bare host (`localhost:8080` without scheme), a non-http scheme (`ftp://`), a malformed port, or an empty string as the service URL when constructing the config object.

Common situations: Config default of `""` left unfilled, forgetting the `https://` scheme prefix, or using `ws://`/custom schemes against the SDK.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/6f00fc34e6a500ff. Report an issue: GitHub.

Appendix: source

Thrown at packages/sdk/python/caveman_cloud/core.py:336

_POLICY_MAX_RESPONSE_BYTES = 1024 * 1024


def _env_workflow() -> str:
    """CAVE_WORKFLOW normalized to the gateway label rule, else the honest default."""
    raw = (os.environ.get("CAVE_WORKFLOW") or "").lower()
    if raw and len(raw) <= 96 and all(c in _WORKFLOW_CHARS for c in raw):
        return raw
    return "unlabeled-workflow"


def _normalized_service_url(value: str, name: str) -> str:
    if value.strip() != value:
        raise ValueError(f"{name} must not contain surrounding whitespace")
    try:
        parsed = urlsplit(value)
        port = parsed.port
    except (TypeError, ValueError) as error:
        raise ValueError(f"{name} must be an absolute http(s) URL") from error
    if (
        parsed.scheme not in ("http", "https")
        or not parsed.hostname
        or parsed.username is not None
        or parsed.password is not None
    ):
        raise ValueError(f"{name} must be an absolute http(s) URL without credentials")
    if parsed.query or parsed.fragment:
        raise ValueError(f"{name} must not contain a query or fragment")
    return value.rstrip("/")


@dataclass
class Cave:
    api_key: str
    base_url: str
    agent: str
    # CAVE_WORKFLOW lets a wrapper (`cave wrap --workflow x`) label every request

View on GitHub (pinned to 3ee70a1026)