{"record":{"id":"c25c74504c172988","repo":"langflow-ai/langflow","slug":"webhook-url-has-no-host","errorCode":null,"errorMessage":"webhook url has no host","messagePattern":"webhook url has no host","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/backend/base/langflow/api/v1/a2a_utils.py","lineNumber":109,"sourceCode":"    component, which would otherwise reopen private/metadata webhooks): resolve the\n    host and reject if any IP is blocked. On top of the floor, run the shared SSRF\n    framework (``validate_and_resolve_url``) for the allowlist / CGNAT / ``is_global``\n    extras and pinned IPs. The returned IPs let the dispatch client pin DNS (closing\n    the rebind window). ``LANGFLOW_A2A_ALLOW_PRIVATE_WEBHOOKS`` skips the IP check for\n    a trusted internal network (returns ``[]``: nothing to pin).\n\n    Raises ``ValueError`` when the URL is unsafe. Returns the validated IPs (framework\n    IPs, falling back to the floor-resolved IPs when the global toggle is off), or\n    ``[]`` when private webhooks are allowed. Used at registration (set_info) and\n    re-run at dispatch.\n    \"\"\"\n    parsed = urlparse(url)\n    if parsed.scheme not in (\"http\", \"https\"):\n        msg = \"webhook url must be http or https\"\n        raise ValueError(msg)\n    if not parsed.hostname:\n        msg = \"webhook url has no host\"\n        raise ValueError(msg)\n    if get_settings_service().settings.a2a_allow_private_webhooks:\n        return []\n    try:\n        # Resolve/validate the SAME host httpx connects to (IDNA/punycode raw_host), not the\n        # unicode urlparse hostname, so an IDN webhook is pinned/resolved by the exact ASCII host\n        # the connection uses (else the pin silently misses: TOCTOU rebind for IDN hosts). httpx.URL\n        # raises InvalidURL (not ValueError) for an IDNA-invalid host, so keep it inside the try.\n        host = webhook_pin_host(url)\n        # Hard floor: reject private/metadata IPs even when global SSRF protection is off\n        # (validate_and_resolve_url returns [] with NO enforcement in that case).\n        # resolve_hostname handles IP-literal hosts too; the blocking resolve runs off-loop.\n        floor_ips = await asyncio.to_thread(resolve_hostname, host)\n        blocked = [ip for ip in floor_ips if is_ip_blocked(ip)]\n        if blocked:\n            msg = f\"webhook url resolves to a blocked address: {', '.join(blocked)}\"\n            raise ValueError(msg)\n        # Then the framework check for allowlist / CGNAT / is_global extras + pinned IPs.\n        _url, validated_ips = await asyncio.to_thread(validate_and_resolve_url, url)","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/v1/a2a_utils.py#L91-L127","documentation":"Raised by validate_webhook_url when urlparse finds no hostname in the webhook URL. This catches malformed URLs where everything lands in scheme/path — the classic example being 'http:/host/hook' (single slash), which urlparse parses with netloc='' and hostname=None. It fires after the scheme check, so the scheme was http/https but the authority component is missing.","triggerScenarios":"Push-notification config with url='http:/host/hook' (one slash), 'https://' with nothing after, or a URL built by string concatenation that dropped the '//'.","commonSituations":"Template/format-string bugs producing http:/{host}/hook; manual typing with a single slash; URL built from a base that already ended in a slash plus naive joining.","solutions":["Use the double-slash authority form: http://host:port/path","Build URLs with a library (urllib.parse.urlunparse, httpx.URL, yarl) rather than string concatenation","Assert parsed.hostname is truthy client-side before submitting the config"],"exampleFix":"# before\nurl = f\"http:/{host}:{port}/hook\"\n# after\nurl = f\"http://{host}:{port}/hook\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef webhook_has_host(url: str) -> bool:\n    return bool(urlparse(url).hostname)","typeGuard":"def has_valid_authority(url: object) -> bool:\n    if not isinstance(url, str):\n        return False\n    p = urlparse(url)\n    return p.scheme in (\"http\", \"https\") and bool(p.hostname)","tryCatchPattern":null,"preventionTips":["Never build URLs by string concatenation; use urlunparse/URL builders","Lint config files for 'http:/' single-slash typos (a cheap regex CI check)","Run the same urlparse assertions client-side that the server runs"],"tags":["a2a","webhook","validation","url"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}