JuliusBrussee/caveman · error · ValueError

must be an absolute http(s) URL without credentials

Error message

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

What it means

This ValueError is raised when the URL parses and uses http(s), but either has no hostname, or embeds a username/password (`https://user:pass@host`). The SDK forbids credentials in the URL itself; authentication is done via the API key instead.

Solutions

  1. Remove the `user:pass@` portion from the URL; pass credentials via `api_key` instead.
  2. Use a plain host URL, e.g. `https://cave.example.com`.
  3. If you intended basic auth, switch to the SDK's authentication mechanism rather than URL credentials.

Example fix

// before
cave = Cave(api_key=key, service_url="https://user:secret@cave.example.com")
// after
cave = Cave(api_key=key, service_url="https://cave.example.com")
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlsplit
def has_url_credentials(v: str) -> bool:
    p = urlsplit(v)
    return p.username is not None or p.password is not None
# reject if True; auth belongs in api_key

Try / catch

try:
    cave = Cave(api_key=key, service_url=url)
except ValueError as e:
    raise ConfigError("remove credentials from service_url; use api_key") from e

Prevention

When it happens

Trigger: Constructing the config with a URL like `https://user:pass@cave.example.com` or a scheme-relative/malformed URL that leaves `parsed.hostname` empty.

Common situations: Baking HTTP basic-auth credentials into the URL copied from another tool, or accidentally including `@` in the host portion.

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/4143e9fa36444da9. Report an issue: GitHub.

Appendix: source

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

        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
    # from an SDK app without a code change. An explicit value always wins. The
    # env value is normalized to the gateway's label rule (lowercase [a-z0-9_-],
    # max 96); an invalid ambient value is ignored rather than 400-ing every
    # request. Mirrors @caveman-ai/sdk (TypeScript).
    default_workflow: str = field(default_factory=lambda: _env_workflow())
    retention: str = "metadata"
    verify_on_init: bool = False

View on GitHub (pinned to 3ee70a1026)