{"record":{"id":"98ff0a5f8eaf15f4","repo":"crewAIInc/crewAI","slug":"url-has-no-hostname-url","errorCode":null,"errorMessage":"URL has no hostname: '{url}'","messagePattern":"URL has no hostname: '(.+?)'","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"lib/crewai-tools/src/crewai_tools/security/safe_path.py","lineNumber":224,"sourceCode":"        return url\n\n    parsed = urlparse(url)\n\n    # Block file:// scheme\n    if parsed.scheme == \"file\":\n        raise ValueError(\n            f\"file:// URLs are not allowed: '{url}'. \"\n            f\"Use a file path instead, or set {_UNSAFE_PATHS_ENV}=true to bypass.\"\n        )\n\n    # Only allow http and https\n    if parsed.scheme not in (\"http\", \"https\"):\n        raise ValueError(\n            f\"URL scheme '{parsed.scheme}' is not allowed. Only http and https are supported.\"\n        )\n\n    if not parsed.hostname:\n        raise ValueError(f\"URL has no hostname: '{url}'\")\n\n    try:\n        addrinfos = socket.getaddrinfo(\n            parsed.hostname, parsed.port or (443 if parsed.scheme == \"https\" else 80)\n        )\n    except socket.gaierror as exc:\n        raise ValueError(f\"Could not resolve hostname: '{parsed.hostname}'\") from exc\n\n    for _family, _, _, _, sockaddr in addrinfos:\n        ip_str = str(sockaddr[0])\n        if _is_private_or_reserved(ip_str):\n            raise ValueError(\n                f\"URL '{url}' resolves to private/reserved IP {ip_str}. \"\n                f\"Access to internal networks is not allowed. \"\n                f\"Set {_UNSAFE_PATHS_ENV}=true to bypass.\"\n            )\n\n    return url","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/crewAIInc/crewAI/blob/754d7323beb2fd042e33444a115ea2d5a47193f0/lib/crewai-tools/src/crewai_tools/security/safe_path.py#L206-L242","documentation":"Thrown by validate_url() in crewai_tools' SSRF guard (safe_path.py) when a URL parses successfully but has no hostname component. The URL validator only permits http/https URLs that resolve to public IPs, and a missing hostname makes the DNS-based private-IP check impossible. This is a fail-fast security check, not a network error.","triggerScenarios":"Calling a tool that fetches URLs (or validate_url directly) with strings like 'http:///path', 'https://:8443/x', 'http:///index.html', or a URL built by string concatenation where the host segment was empty or dropped (e.g. f\"http://{host}/api\" with host='').","commonSituations":"Building URLs from templates or config where the host variable is empty/None; stripping a hostname during URL sanitization; copy-paste typos with double slashes after the scheme; test fixtures using placeholder URLs without hosts.","solutions":["Inspect the exact URL string being passed and fix the malformed host segment (ensure scheme is followed by a non-empty hostname, e.g. 'https://example.com/path').","If the URL is built dynamically, log or assert the host component is non-empty before constructing the URL.","If you genuinely need to load a local file, pass a file path instead of a file:// URL (the guard also rejects file:// separately).","As a last-resort bypass in trusted environments only, set the documented unsafe-paths env var (referenced by _UNSAFE_PATHS_ENV) to 'true'."],"exampleFix":"// before\nurl = f\"http://{base_host}/data\"  # base_host is '' -> 'http:///data'\n\n# after\nif not base_host:\n    raise ValueError(\"base_host must be set\")\nurl = f\"http://{base_host}/data\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef has_hostname(url: str) -> bool:\n    return bool(urlparse(url).hostname)","typeGuard":null,"tryCatchPattern":"try:\n    validate_url(url)\nexcept ValueError as e:\n    if \"no hostname\" in str(e):\n        # fix the URL string, do not retry unchanged\n        raise ValueError(f\"Malformed URL, missing host: {url!r}\") from e\n    raise","preventionTips":["Never build URLs by naive f-string concatenation without asserting the host segment is non-empty.","Validate URLs with urlparse() (scheme in {http,https} and hostname truthy) before passing them to crewai fetch tools.","Sanitize LLM-provided URLs through the same validation before use."],"tags":["url-validation","security","ssrf","input-validation"],"backgroundTag":null,"analyzedSha":"754d7323beb2fd042e33444a115ea2d5a47193f0","analyzedAt":"2026-08-15T04:06:56.746Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}