From 459cf3402b7aaabd8c5efecd7100c47e40bdc938 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:00:52 +0530 Subject: [PATCH] fix(web): preserve extract result input order --- tests/tools/test_web_tools_dict_urls.py | 6 ++++++ tools/web_tools.py | 26 ++++++++++++++++--------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/tests/tools/test_web_tools_dict_urls.py b/tests/tools/test_web_tools_dict_urls.py index 93fb69924eb..d58f0695442 100644 --- a/tests/tools/test_web_tools_dict_urls.py +++ b/tests/tools/test_web_tools_dict_urls.py @@ -85,6 +85,12 @@ async def test_web_extract_reports_invalid_items_without_dispatching_them(extrac ])) assert extract_provider.received_urls == ["https://example.com/good"] + assert [entry["url"] for entry in result["results"]] == [ + "https://example.com/good", + "", + "", + "", + ] errors = [entry["error"] for entry in result["results"] if entry["error"]] assert errors == [ "Invalid URL item at index 1: expected a URL string or an object " diff --git a/tools/web_tools.py b/tools/web_tools.py index 71a53f982ee..64dc5e1c726 100644 --- a/tools/web_tools.py +++ b/tools/web_tools.py @@ -778,11 +778,12 @@ async def web_extract_tool( from agent.redact import _PREFIX_RE from urllib.parse import unquote normalized_urls: List[str] = [] - invalid_urls: List[Dict[str, Any]] = [] + normalized_indices: List[int] = [] + invalid_urls: Dict[int, Dict[str, Any]] = {} for index, item in enumerate(urls): _url = _web_extract_url(item) if _url is None: - invalid_urls.append({ + invalid_urls[index] = { "url": "", "title": "", "content": "", @@ -790,7 +791,7 @@ async def web_extract_tool( f"Invalid URL item at index {index}: expected a URL string " "or an object with a string 'url' or 'href' field" ), - }) + } continue normalized_url = normalize_url_for_request(_url) if ( @@ -816,6 +817,7 @@ async def web_extract_tool( ), }) normalized_urls.append(normalized_url) + normalized_indices.append(index) debug_call_data = { "parameters": { @@ -837,15 +839,17 @@ async def web_extract_tool( # ── SSRF protection — filter out private/internal URLs before any backend ── safe_urls = [] - ssrf_blocked: List[Dict[str, Any]] = [] - for url in normalized_urls: + safe_indices = [] + ssrf_blocked: Dict[int, Dict[str, Any]] = {} + for index, url in zip(normalized_indices, normalized_urls): if not await async_is_safe_url(url): - ssrf_blocked.append({ + ssrf_blocked[index] = { "url": url, "title": "", "content": "", "error": "Blocked: URL targets a private or internal network address", - }) + } else: safe_urls.append(url) + safe_indices.append(index) # Dispatch only safe URLs to the configured backend if not safe_urls: @@ -939,9 +943,13 @@ async def web_extract_tool( provider.extract, safe_urls, format=format ) - # Merge rejected and SSRF-blocked inputs back into the result envelope. + # Reconstruct the original input order across invalid, blocked, and + # provider-processed entries. Providers are expected to preserve the + # order of the safe URL list they receive. if invalid_urls or ssrf_blocked: - results = invalid_urls + ssrf_blocked + results + safe_results = dict(zip(safe_indices, results)) + by_index = {**safe_results, **ssrf_blocked, **invalid_urls} + results = [by_index[index] for index in range(len(urls))] response = {"results": results}