langflow-ai/langflow · error · ValueError
webhook url has an invalid host
Error message
webhook url has an invalid host: {exc} What it means
Raised by validate_webhook_url when httpx.URL rejects the webhook host as IDNA-invalid (httpx.InvalidURL), translated to ValueError because callers only guard ValueError. Without the translation an exotic/illegal hostname would escape as a 500 or crash the dispatch loop instead of failing closed as an unsafe webhook. It complements the urlparse check by validating the exact ASCII host httpx would connect to.
Solutions
- Fix the hostname to a valid DNS name: replace underscores with hyphens, remove stray unicode characters
- Validate with httpx.URL(url) client-side — if it raises InvalidURL the server will reject it too
- Register the webhook under a clean canonical name (CNAME to the odd internal host if needed)
Example fix
# before url = "https://my_hook.example.com/cb" # after url = "https://my-hook.example.com/cb"
Defensive patterns
Strategy: validation
Validate before calling
import httpx
def webhook_host_idna_safe(url: str) -> bool:
try:
httpx.URL(url)
return True
except httpx.InvalidURL:
return False Try / catch
try:
await client.tasks.set_push_notification(task_id, cfg)
except InvalidParamsError as e:
if "invalid host" in str(e):
cfg.url = sanitize_hostname(cfg.url) # underscores->hyphens, strip unicode
await client.tasks.set_push_notification(task_id, cfg)
else:
raise Prevention
- Mirror the server: validate with httpx.URL before submitting any webhook
- Ban underscores/unicode in webhook hostnames in your config linting
- Use CNAMEs with clean names for oddly-named internal hosts
When it happens
Trigger: A webhook URL whose hostname contains characters that cannot be IDNA-encoded — e.g. embedded underscores ('my_hook.example.com' is actually accepted by some resolvers but rejected here), labels >63 chars, prohibited code points, or stray unicode punctuation in the host.
Common situations: Hostnames with underscores common in internal naming (service_name.internal); copy-pasting a URL with a smart quote/zero-width character; very long auto-generated subdomains exceeding per-label limits.
Related errors
- webhook url has no host
- webhook url must be http or https
- str(exc)
- webhook url is not allowed
- webhook url resolves to a blocked address
AI-assisted analysis of langflow-ai/langflow@976ec789d2 (2026-08-14).
Data as JSON: /api/errors/01e9a4cb274c6b1e.
Report an issue: GitHub.
Appendix: source
Thrown at src/backend/base/langflow/api/v1/a2a_utils.py:132
# 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)
and what the JSON-RPC route enforces, so the two can't drift. Plaintext read,
no decrypt. No folder / missing folder -> ``"none"`` (public).
"""
if flow.folder_id is None:
return "none"
# Query the folder explicitly; lazy-loading flow.folder would raise in async.
folder = (await session.exec(select(Folder).where(Folder.id == flow.folder_id))).first()View on GitHub (pinned to 976ec789d2)