{"record":{"id":"f173fc12e75da290","repo":"langflow-ai/langflow","slug":"webhook-url-must-be-http-or-https","errorCode":null,"errorMessage":"webhook url must be http or https","messagePattern":"webhook url must be http or https","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/backend/base/langflow/api/v1/a2a_utils.py","lineNumber":106,"sourceCode":"    The A2A endpoint is public, so the webhook target is caller-controlled. Require\n    http/https, then enforce a hard IP floor that does NOT depend on the global\n    ``LANGFLOW_SSRF_PROTECTION_ENABLED`` toggle (ops disable it for the API Request\n    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)}\"","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/langflow-ai/langflow/blob/976ec789d2886a86de109c044d089d68e96c9a35/src/backend/base/langflow/api/v1/a2a_utils.py#L88-L124","documentation":"Raised by validate_webhook_url in a2a_utils.py when an A2A push-notification webhook URL's scheme is anything other than http or https. The scheme check runs before DNS resolution, so this is a pure URL-format rejection; the ValueError is surfaced to the client as a JSON-RPC InvalidParamsError at registration (set_info) or logged-and-dropped at dispatch.","triggerScenarios":"tasks/pushNotificationConfig/set (or message/send with push config) with url like ftp://host/hook, ws://host/hook, or file:///path — any scheme outside {http, https}.","commonSituations":"Copying a WebSocket endpoint from the A2A streaming docs into the push config; trailing whitespace or a missing '//' making urlparse parse 'http:/host' into an unexpected scheme/path; templating bugs that drop the scheme entirely.","solutions":["Set the webhook URL to an http:// or https:// endpoint that can receive POST callbacks","Trim/validate the URL client-side before registration (scheme check is the cheapest guard)","For local dev use http://<lan-ip>:port, not ws:// — push notifications are HTTP POSTs, not sockets"],"exampleFix":"# before\nurl = \"ws://receiver.internal:9000/hook\"\n# after\nurl = \"http://receiver.internal:9000/hook\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef webhook_scheme_ok(url: str) -> bool:\n    return urlparse(url).scheme in (\"http\", \"https\")","typeGuard":"def is_http_url(url: object) -> bool:\n    return isinstance(url, str) and urlparse(url).scheme in (\"http\", \"https\") and bool(urlparse(url).netloc)","tryCatchPattern":"try:\n    await client.tasks.set_push_notification(task_id, cfg)\nexcept InvalidParamsError as e:\n    if \"must be http or https\" in str(e):\n        cfg.url = re.sub(r\"^\\w+://\", \"https://\", cfg.url)\n        await client.tasks.set_push_notification(task_id, cfg)\n    else:\n        raise","preventionTips":["Construct webhook URLs only via URL builders that enforce the scheme","Reject ws/ftp schemes in your config schema before they reach the API","Reserve a single well-known https webhook base URL in configuration"],"tags":["a2a","webhook","validation","url"],"backgroundTag":null,"analyzedSha":"976ec789d2886a86de109c044d089d68e96c9a35","analyzedAt":"2026-08-14T18:23:12.227Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}