{"record":{"id":"c0310d0d95fb348f","repo":"docling-project/docling","slug":"url-must-contain-a-valid-hostname","errorCode":null,"errorMessage":"URL must contain a valid hostname","messagePattern":"URL must contain a valid hostname","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"docling/backend/utils/image_resource_loader.py","lineNumber":54,"sourceCode":"def validate_url_safety(url: str) -> None:\n    \"\"\"Reject URLs that resolve to a non-public IP address.\n\n    Guards against SSRF by requiring the URL's host to resolve to a globally\n    routable address. Private, loopback, link-local, reserved, multicast, and\n    unspecified addresses are refused.\n\n    Args:\n        url: The URL whose host is validated.\n\n    Raises:\n        ValueError: If the URL has no hostname, the hostname cannot be\n            resolved, or it resolves to a restricted (non-global) IP address.\n    \"\"\"\n    parsed = urlparse(url)\n    hostname = parsed.hostname\n\n    if not hostname:\n        raise ValueError(\"URL must contain a valid hostname\")\n\n    try:\n        ip = ipaddress.ip_address(hostname)\n    except ValueError:\n        try:\n            ip_str = socket.gethostbyname(hostname)\n            ip = ipaddress.ip_address(ip_str)\n        except (socket.gaierror, socket.herror) as e:\n            raise ValueError(f\"Cannot resolve hostname: {hostname}\") from e\n\n    if not (\n        ip.is_global\n        and not (\n            ip.is_private\n            or ip.is_loopback\n            or ip.is_link_local\n            or ip.is_reserved\n            or ip.is_multicast","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/docling-project/docling/blob/61d76f1ff3f8428065465889f7b4577da7df704c/docling/backend/utils/image_resource_loader.py#L36-L72","documentation":"ValueError from validate_url_safety (docling/backend/utils/image_resource_loader.py) when a URL parsed with urlparse() has no hostname. It is the first stage of SSRF protection for remotely fetched images referenced by documents (e.g. HTML backends) before any DNS resolution or HTTP request occurs.","triggerScenarios":"A document references an image with a malformed URL such as '/img/logo.png', 'mailto:x@y', 'file:///tmp/x.png', or 'http:///path' (empty host), and remote fetching is enabled so validate_url_safety runs.","commonSituations":"HTML/ODT sources with relative or scheme-relative image srcs that were not resolved against a base URL before loading; hand-authored documents with typos in image links.","solutions":["Supply a proper base_path/base URL when converting so relative image srcs are resolved with urljoin before fetching.","Fix the document's image references to absolute http(s) URLs.","Catch ValueError around conversion of untrusted HTML and log the offending src."],"exampleFix":"# before\nloader.load_image_data('images/pic.png', None)  # ValueError: no hostname\n\n# after\nabs_loc = loader.resolve_relative_path('images/pic.png', 'https://example.com/page.html')\ndata = loader.load_image_data(abs_loc, 'https://example.com/page.html')","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\np = urlparse(url)\nif not p.hostname:\n    raise ValueError(f'image URL lacks hostname: {url}')","typeGuard":"def has_hostname(url: str) -> bool:\n    return urlparse(url).hostname is not None","tryCatchPattern":"from urllib.parse import urlparse\nif not urlparse(src).hostname:\n    continue  # skip malformed image reference\nloader.load_image_data(src, base)","preventionTips":["Always pass a base URL when converting HTML with relative image srcs","Lint documents for malformed image links before conversion","Skip non-http(s) image schemes up front"],"tags":["ssrf","url-validation","images","network"],"backgroundTag":null,"analyzedSha":"61d76f1ff3f8428065465889f7b4577da7df704c","analyzedAt":"2026-08-14T23:53:18.727Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}