langflow-ai/langflow · error · InvalidParamsError

str(exc)

Error message

str(exc)

What it means

Raised by _SafePushConfigStore.set_info when registering an A2A task push-notification webhook whose URL fails validate_webhook_url. The ValueError (bad scheme, no host, blocked/private IP, IDNA-invalid host, or SSRF allowlist denial) is translated into the JSON-RPC InvalidParamsError so the client gets a spec-level error instead of a 500. This is the SSRF guard on a public, caller-controlled webhook URL, enforced at registration time (and re-checked at dispatch).

Source

Thrown at src/backend/base/langflow/api/v1/a2a.py:385

    via ``get_info_for_dispatch`` and is unaffected.
    """
    return f"{resolve_user_scope(context)}:{context.state.get('flow_id', '')}"


class _SafePushConfigStore(InMemoryPushNotificationConfigStore):
    """Push-notification config store that SSRF-guards the webhook URL at registration.

    The webhook target is caller-controlled on a public endpoint, so reject one that
    resolves to a private/loopback/link-local address before storing it (rather than
    letting the sender POST there). In-memory + per-worker for now; durable cross-worker
    push configs are a later slice (like the streaming queue manager).
    """

    async def set_info(self, task_id: str, notification_config: pb.TaskPushNotificationConfig, context) -> None:
        try:
            await validate_webhook_url(notification_config.url)
        except ValueError as exc:
            raise InvalidParamsError(message=str(exc)) from exc
        await super().set_info(task_id, notification_config, context)


class _SafePushNotificationSender(BasePushNotificationSender):
    """Re-validate + DNS-pin the webhook at dispatch, closing the registration-time rebind gap.

    ``_SafePushConfigStore`` validates at registration, but a host can re-resolve to a
    private/metadata IP afterwards (DNS rebinding). Re-resolve here and pin the connection
    to the just-validated IPs (via the shared SSRF transport) so a rebind can't land on an
    internal address. A webhook that now resolves to a blocked address is dropped (logged),
    matching the SDK's swallow-and-return-False on a failed send.
    """

    async def _dispatch_notification(self, event, push_info, task_id) -> bool:
        url = push_info.url
        try:
            validated_ips = await validate_webhook_url(url)
        except ValueError:

View on GitHub (pinned to 976ec789d2)

Solutions

  1. Use a public https:// webhook URL that resolves to a globally routable IP
  2. For trusted internal networks, set LANGFLOW_A2A_ALLOW_PRIVATE_WEBHOOKS=true on the server (it skips the IP check)
  3. Fix malformed URLs: include scheme http/https and a real hostname; avoid IP-literal private addresses

Example fix

# before
config.url = "http://localhost:9000/a2a-hook"
# after
config.url = "https://hooks.example.com/a2a-hook"
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse
import ipaddress, socket

def webhook_url_safe(url: str) -> bool:
    p = urlparse(url)
    if p.scheme not in ("http", "https") or not p.hostname:
        return False
    try:
        infos = socket.getaddrinfo(p.hostname, None)
    except socket.gaierror:
        return False
    return all(
        not ipaddress.ip_address(i[4][0]).is_private
        and not ipaddress.ip_address(i[4][0]).is_loopback
        and not ipaddress.ip_address(i[4][0]).is_link_local
        for i in infos
    )

Try / catch

try:
    await client.tasks.set_push_notification(task_id, config)
except InvalidParamsError as e:
    if "webhook url" in str(e):
        config.url = public_tunnel_url()   # replace internal URL with public https
        await client.tasks.set_push_notification(task_id, config)
    else:
        raise

Prevention

When it happens

Trigger: A2A message/send or tasks/pushNotificationConfig/set with notification_config.url pointing at e.g. http://localhost:9000/hook, http://169.254.169.254/..., ftp://..., a URL with no hostname, or a host that resolves to a private/CGNAT range when SSRF protection is on.

Common situations: Local testing with a loopback webhook against a hardened server; pointing webhooks at internal hostnames; typos like 'http:/host' (missing slash yields no host); DNS entries that resolve to 10.x/172.16.x/192.168.x ranges.

Related errors


AI-assisted analysis of langflow-ai/langflow@976ec789d2 (2026-08-14). Data as JSON: /api/errors/be8d51e78b5bc21b. Report an issue: GitHub.