{"record":{"id":"6f00fc34e6a500ff","repo":"JuliusBrussee/caveman","slug":"name-must-be-an-absolute-http-s-url-6f00fc","errorCode":null,"errorMessage":"{name} must be an absolute http(s) URL","messagePattern":"(.+?) must be an absolute http\\(s\\) URL","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"packages/sdk/python/caveman_cloud/core.py","lineNumber":314,"sourceCode":"_POLICY_MAX_RESPONSE_BYTES = 1024 * 1024\n\n\ndef _env_workflow() -> str:\n    \"\"\"CAVE_WORKFLOW normalized to the gateway label rule, else the honest default.\"\"\"\n    raw = (os.environ.get(\"CAVE_WORKFLOW\") or \"\").lower()\n    if raw and len(raw) <= 96 and all(c in _WORKFLOW_CHARS for c in raw):\n        return raw\n    return \"unlabeled-workflow\"\n\n\ndef _normalized_service_url(value: str, name: str) -> str:\n    if value.strip() != value:\n        raise ValueError(f\"{name} must not contain surrounding whitespace\")\n    try:\n        parsed = urlsplit(value)\n        port = parsed.port\n    except (TypeError, ValueError) as error:\n        raise ValueError(f\"{name} must be an absolute http(s) URL\") from error\n    if (\n        parsed.scheme not in (\"http\", \"https\")\n        or not parsed.hostname\n        or parsed.username is not None\n        or parsed.password is not None\n    ):\n        raise ValueError(f\"{name} must be an absolute http(s) URL without credentials\")\n    if parsed.query or parsed.fragment:\n        raise ValueError(f\"{name} must not contain a query or fragment\")\n    return value.rstrip(\"/\")\n\n\n@dataclass\nclass Cave:\n    api_key: str\n    base_url: str\n    agent: str\n    # CAVE_WORKFLOW lets a wrapper (`cave wrap --workflow x`) label every request","sourceCodeStart":296,"sourceCodeEnd":332,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/sdk/python/caveman_cloud/core.py#L296-L332","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Fix the URL so the port is a valid integer between 1 and 65535, or omit the port entirely for the default scheme port.","If building the URL dynamically, only append :port when the value is set.","Ensure the value is a str before passing it to Cave."],"exampleFix":"# before\nbase_url = f\"https://{host}:{port}\"  # port is None -> ':None'\n# after\nbase_url = f\"https://{host}:{port}\" if port else f\"https://{host}\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlsplit\n\ndef parse_port(value: str) -> int | None:\n    try:\n        urlsplit(value).port\n        return True\n    except (TypeError, ValueError):\n        return False\n\nif not parse_port(base_url):\n    raise ValueError(f\"base_url has a malformed port: {base_url!r}\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only append ':port' when the port variable is a real integer.","Validate user-supplied base URLs at config load, not at first request."],"tags":["python","sdk","url","validation","port"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}