langflow-ai/langflow · error · ValueError
webhook url resolves to a blocked address: {', '.join(blocke
Error message
webhook url resolves to a blocked address: {', '.join(blocked)} What it means
Raised by validate_webhook_url when the webhook host resolves (via resolve_hostname, off-loop) to at least one IP on the SSRF blocklist — private ranges (10/8, 172.16/12, 192.168/16), loopback, link-local (incl. 169.254.169.254 metadata), or other non-routable space. This is a hard floor enforced even when the framework's global SSRF protection is off, because the webhook target is caller-controlled on a public endpoint. The message lists the blocked IPs.
Source
Thrown at src/backend/base/langflow/api/v1/a2a_utils.py:125
if not parsed.hostname:
msg = "webhook url has no host"
raise ValueError(msg)
if get_settings_service().settings.a2a_allow_private_webhooks:
return []
try:
# Resolve/validate the SAME host httpx connects to (IDNA/punycode raw_host), not the
# unicode urlparse hostname, so an IDN webhook is pinned/resolved by the exact ASCII host
# the connection uses (else the pin silently misses: TOCTOU rebind for IDN hosts). httpx.URL
# raises InvalidURL (not ValueError) for an IDNA-invalid host, so keep it inside the try.
host = webhook_pin_host(url)
# Hard floor: reject private/metadata IPs even when global SSRF protection is off
# (validate_and_resolve_url returns [] with NO enforcement in that case).
# resolve_hostname handles IP-literal hosts too; the blocking resolve runs off-loop.
floor_ips = await asyncio.to_thread(resolve_hostname, host)
blocked = [ip for ip in floor_ips if is_ip_blocked(ip)]
if blocked:
msg = f"webhook url resolves to a blocked address: {', '.join(blocked)}"
raise ValueError(msg)
# Then the framework check for allowlist / CGNAT / is_global extras + pinned IPs.
_url, validated_ips = await asyncio.to_thread(validate_and_resolve_url, url)
except httpx.InvalidURL as exc:
# Callers only guard ValueError; without translating this an IDNA-invalid host would 500 the
# caller (or escape dispatch) instead of failing closed as an unsafe webhook.
msg = f"webhook url has an invalid host: {exc}"
raise ValueError(msg) from exc
except SSRFProtectionError as exc:
msg = f"webhook url is not allowed: {exc}"
raise ValueError(msg) from exc
# Fall back to the floor IPs so dispatch can still DNS-pin with the global toggle off.
return validated_ips or floor_ips
async def folder_auth_type(flow: Flow, session: AsyncSession) -> str:
"""Read the flow's folder ``auth_type`` (``"none"`` | ``"apikey"`` | ``"oauth"``).
The single source of truth for what the card advertises (resolve_card_security)View on GitHub (pinned to 976ec789d2)
Solutions
- Point the webhook at a publicly routable https URL
- For trusted internal networks, set LANGFLOW_A2A_ALLOW_PRIVATE_WEBHOOKS=true (skips the IP floor, returns no pinned IPs)
- If the hostname has split-horizon DNS, use a name whose public resolution is global, or front the internal receiver with a public tunnel (e.g. an HTTPS tunnel service)
Example fix
# before url = "http://192.168.1.5:9000/a2a-hook" # after url = "https://a2a-hooks.example.com/hook" # tunnels to the internal receiver
Defensive patterns
Strategy: validation
Validate before calling
import ipaddress, socket
from urllib.parse import urlparse
def webhook_ips_public(url: str) -> bool:
host = urlparse(url).hostname or ""
try:
ips = {i[4][0] for i in socket.getaddrinfo(host, None)}
except socket.gaierror:
return False
bad = {ip for ip in ips if ipaddress.ip_address(ip).is_private or ipaddress.ip_address(ip).is_loopback or ipaddress.ip_address(ip).is_link_local}
return not bad Try / catch
try:
await client.tasks.set_push_notification(task_id, cfg)
except InvalidParamsError as e:
if "blocked address" in str(e):
cfg.url = public_webhook_for(cfg.url) # tunnel/public endpoint
await client.tasks.set_push_notification(task_id, cfg)
else:
raise Prevention
- Default to public https webhook endpoints; treat private targets as a deployment smell
- For internal receivers use an outbound tunnel rather than weakening the SSRF floor
- Watch for split-horizon DNS: validate the resolution the server will see, not your laptop's
When it happens
Trigger: Registering a push config whose URL is http://localhost/…, http://127.0.0.1:8080/…, http://192.168.1.5/hook, a corporate internal hostname resolving to 10.x, or http://169.254.169.254/latest/meta-data (cloud metadata theft attempt).
Common situations: Local development against a deployed server (localhost webhook); pointing at an internal receiver hostname that shares public DNS with an A record to a private IP; genuine SSRF probing of the deployment's internal network.
Related errors
- str(exc)
- webhook url is not allowed: {exc}
- webhook url must be http or https
- webhook url has no host
- webhook url has an invalid host: {exc}
AI-assisted analysis of langflow-ai/langflow@976ec789d2 (2026-08-14).
Data as JSON: /api/errors/884e5743ada1a649.
Report an issue: GitHub.