fix: repair URL authority whitespace before web fetches (#46363)

Port from openclaw/openclaw#91950: normalize LLM-generated URLs like 'https:// docs.example' before web tool safety checks while preserving path and query encoding semantics.
This commit is contained in:
Teknium 2026-07-07 02:39:36 -07:00 committed by GitHub
parent a796e0b796
commit 8fc1cb754b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 32 additions and 0 deletions

View file

@ -43,6 +43,30 @@ class TestNormalizeUrlForRequest:
== "https://xn--mnich-kva.example/K%C3%B6ln"
)
def test_repairs_space_between_scheme_and_authority(self):
assert (
normalize_url_for_request("https:// docs.openclaw.ai")
== "https://docs.openclaw.ai"
)
def test_repairs_tab_between_scheme_and_authority(self):
assert (
normalize_url_for_request("https:// docs.openclaw.ai/path")
== "https://docs.openclaw.ai/path"
)
def test_trims_but_preserves_path_and_query_space_semantics(self):
assert (
normalize_url_for_request(" https://example.com/a b?q=c d ")
== "https://example.com/a%20b?q=c%20d"
)
def test_does_not_collapse_embedded_scheme_separator_in_query(self):
assert (
normalize_url_for_request("https://example.com/r?next=https:// evil.example")
== "https://example.com/r?next=https://%20evil.example"
)
class TestIsSafeUrl:
def test_public_url_allowed(self):

View file

@ -28,6 +28,7 @@ import logging
import os
import socket
import asyncio
import re
from typing import Any, Optional
from urllib.parse import parse_qsl, quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
@ -52,6 +53,13 @@ def normalize_url_for_request(url: str) -> str:
if not raw:
return raw
# Models sometimes emit otherwise valid URLs with whitespace between the
# scheme separator and authority (``https:// docs.example``). That position
# is never meaningful in HTTP(S) URLs, and repairing it before parsing keeps
# web tools from failing on a formatting artifact while leaving path/query
# whitespace to the normal percent-encoding path below.
raw = re.sub(r"^([A-Za-z][A-Za-z0-9+.-]*://)\s+", r"\1", raw)
try:
parsed = urlsplit(raw)
except ValueError: