JuliusBrussee/caveman · error · ValueError

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

Error message

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

What it means

ValueError from _normalized_service_url in packages/sdk/python/caveman_cloud/core.py, raised from the urlsplit failure handler. Accessing parsed.port triggers Python's port parsing, which raises ValueError for malformed ports (non-numeric, out of range, or empty); TypeError propagates the same way for non-string input. The message says the value must be an absolute http(s) URL because that is the accepted shape, even though the proximate cause is usually a bad port segment.

Source

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

_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 27d5a3981a)

Solutions

  1. Fix the URL so the port is a valid integer between 1 and 65535, or omit the port entirely for the default scheme port.
  2. If building the URL dynamically, only append :port when the value is set.
  3. Ensure the value is a str before passing it to Cave.

Example fix

# before
base_url = f"https://{host}:{port}"  # port is None -> ':None'
# after
base_url = f"https://{host}:{port}" if port else f"https://{host}"
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlsplit

def parse_port(value: str) -> int | None:
    try:
        urlsplit(value).port
        return True
    except (TypeError, ValueError):
        return False

if not parse_port(base_url):
    raise ValueError(f"base_url has a malformed port: {base_url!r}")

Prevention

When it happens

Trigger: A URL like "https://gateway.example.com:abc/" (non-numeric port), "https://host:99999/" (out-of-range port), "https://host:/" (empty port), or passing None/an int instead of a string where a URL is expected.

Common situations: Composing base_url from parts with an unset or placeholder port variable (f"https://{host}:{port}" with port=None rendering as 'None'); templating mistakes in deployment config; typos after the last colon.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/6f00fc34e6a500ff. Report an issue: GitHub.