mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(http_handler): prevent empty params from stripping URL query string
When `HTTPHandler.get()` / `AsyncHTTPHandler.get()` call
`extract_query_params(url)` on a URL whose query string doesn't contain
`key=value` pairs (e.g. CDN image processing URLs like
`?imageMogr2/thumbnail/800x800/format/jpg`), `parse_qsl` returns an
empty dict. Passing `params={}` to httpx causes it to **remove** the
original query string entirely, so the downstream server receives a bare
URL without any processing parameters.
This breaks image downloads for providers like Bedrock that rely on
`BedrockImageProcessor` to fetch images via `get_image_details_async()`.
The CDN returns the unprocessed original image (e.g. 3.8 MB PNG instead
of 144 KB JPEG), which after base64 encoding exceeds Bedrock's 5 MB
image size limit.
Fix: pass `params=None` instead of `params={}` when no key=value query
parameters are extracted, so httpx preserves the original query string.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5183a6e850
commit
276b4f8c13
2 changed files with 104 additions and 4 deletions
|
|
@ -423,8 +423,15 @@ class AsyncHTTPHandler:
|
|||
params = params or {}
|
||||
params.update(HTTPHandler.extract_query_params(url))
|
||||
|
||||
# Only pass params when non-empty; passing an empty dict causes httpx to
|
||||
# strip the original query-string from the URL entirely, which breaks
|
||||
# URLs whose query-string doesn't use key=value pairs (e.g. CDN image
|
||||
# processing URLs like `?imageMogr2/thumbnail/800x800/format/jpg`).
|
||||
response = await self.client.get(
|
||||
url, params=params, headers=headers, follow_redirects=_follow_redirects # type: ignore
|
||||
url,
|
||||
params=params or None,
|
||||
headers=headers,
|
||||
follow_redirects=_follow_redirects, # type: ignore
|
||||
)
|
||||
return response
|
||||
|
||||
|
|
@ -967,9 +974,13 @@ class HTTPHandler:
|
|||
params = params or {}
|
||||
params.update(self.extract_query_params(url))
|
||||
|
||||
# Only pass params when non-empty; passing an empty dict causes httpx to
|
||||
# strip the original query-string from the URL entirely, which breaks
|
||||
# URLs whose query-string doesn't use key=value pairs (e.g. CDN image
|
||||
# processing URLs like `?imageMogr2/thumbnail/800x800/format/jpg`).
|
||||
response = self.client.get(
|
||||
url,
|
||||
params=params,
|
||||
params=params or None,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import os
|
|||
import pathlib
|
||||
import ssl
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
import certifi
|
||||
import httpx
|
||||
|
|
@ -15,7 +15,7 @@ sys.path.insert(
|
|||
) # Adds the parent directory to the system path
|
||||
import litellm
|
||||
from litellm.llms.custom_httpx.aiohttp_transport import LiteLLMAiohttpTransport
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, get_ssl_configuration
|
||||
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler, get_ssl_configuration
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -635,3 +635,92 @@ async def test_httpx_handler_uses_env_user_agent(monkeypatch):
|
|||
assert req.headers.get("User-Agent") == "Claude Code"
|
||||
finally:
|
||||
await handler.close()
|
||||
|
||||
def test_extract_query_params_standard_url():
|
||||
"""Test extract_query_params with standard key=value query strings."""
|
||||
result = HTTPHandler.extract_query_params(
|
||||
"https://example.com/path?foo=1&bar=2"
|
||||
)
|
||||
assert result == {"foo": "1", "bar": "2"}
|
||||
|
||||
|
||||
def test_extract_query_params_no_keyvalue_pairs():
|
||||
"""Test extract_query_params returns empty dict for non key=value query strings.
|
||||
|
||||
CDN image processing URLs (e.g. Qiniu) use path-style query strings like
|
||||
`?imageMogr2/thumbnail/800x800/format/jpg` which don't contain `=`.
|
||||
`parse_qsl` cannot parse these and returns an empty list.
|
||||
"""
|
||||
result = HTTPHandler.extract_query_params(
|
||||
"https://cdn.example.com/img.png?imageMogr2/thumbnail/800x800/format/jpg"
|
||||
)
|
||||
assert result == {}
|
||||
|
||||
|
||||
def test_extract_query_params_no_query_string():
|
||||
"""Test extract_query_params with no query string at all."""
|
||||
result = HTTPHandler.extract_query_params("https://example.com/path")
|
||||
assert result == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_get_preserves_query_string_when_no_keyvalue_params():
|
||||
"""Test that AsyncHTTPHandler.get() preserves the original query string
|
||||
when extract_query_params returns an empty dict.
|
||||
|
||||
Regression test: previously, passing `params={}` to httpx caused it to
|
||||
strip the original query string from the URL entirely. This broke CDN
|
||||
image processing URLs that use non-standard query strings without `=`.
|
||||
"""
|
||||
handler = AsyncHTTPHandler(timeout=httpx.Timeout(timeout=10.0))
|
||||
|
||||
cdn_url = "https://cdn.example.com/img.png?imageMogr2/thumbnail/800x800%3E/format/jpg"
|
||||
|
||||
mock_response = MagicMock(spec=httpx.Response)
|
||||
mock_response.status_code = 200
|
||||
|
||||
with patch.object(handler.client, "get", new_callable=AsyncMock, return_value=mock_response) as mock_get:
|
||||
await handler.get(cdn_url, follow_redirects=True)
|
||||
|
||||
# Verify the URL passed to httpx still contains the query string
|
||||
call_args = mock_get.call_args
|
||||
actual_url = call_args[0][0] if call_args[0] else call_args[1].get("url")
|
||||
assert "imageMogr2" in str(actual_url), (
|
||||
f"Query string was stripped from URL. Got: {actual_url}"
|
||||
)
|
||||
|
||||
# Verify params is None (not empty dict) so httpx doesn't strip query string
|
||||
actual_params = call_args[1].get("params")
|
||||
assert actual_params is None, (
|
||||
f"Expected params=None when no key=value pairs in query string, got: {actual_params}"
|
||||
)
|
||||
|
||||
|
||||
def test_sync_get_preserves_query_string_when_no_keyvalue_params():
|
||||
"""Test that HTTPHandler.get() preserves the original query string
|
||||
when extract_query_params returns an empty dict.
|
||||
|
||||
Regression test: same issue as async variant above.
|
||||
"""
|
||||
handler = HTTPHandler(timeout=httpx.Timeout(timeout=10.0))
|
||||
|
||||
cdn_url = "https://cdn.example.com/img.png?imageMogr2/thumbnail/800x800%3E/format/jpg"
|
||||
|
||||
mock_response = MagicMock(spec=httpx.Response)
|
||||
mock_response.status_code = 200
|
||||
|
||||
with patch.object(handler.client, "get", return_value=mock_response) as mock_get:
|
||||
handler.get(cdn_url, follow_redirects=True)
|
||||
|
||||
# Verify the URL passed to httpx still contains the query string
|
||||
call_args = mock_get.call_args
|
||||
actual_url = call_args[0][0] if call_args[0] else call_args[1].get("url")
|
||||
assert "imageMogr2" in str(actual_url), (
|
||||
f"Query string was stripped from URL. Got: {actual_url}"
|
||||
)
|
||||
|
||||
# Verify params is None (not empty dict) so httpx doesn't strip query string
|
||||
actual_params = call_args[1].get("params")
|
||||
assert actual_params is None, (
|
||||
f"Expected params=None when no key=value pairs in query string, got: {actual_params}"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue