fix(s3_v2): use prepared URL for SigV4-signed S3 requests (#25074)

This commit is contained in:
Neha Prasad 2026-04-05 07:09:37 +05:30 committed by GitHub
parent cf94f4d8b7
commit d6351a3966
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 51 additions and 6 deletions

View file

@ -403,9 +403,8 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
# Prepare the signed headers
signed_headers = dict(aws_request.headers.items())
# Make the request
response = await self.async_httpx_client.put(
url, data=json_string, headers=signed_headers
prepped.url, data=json_string, headers=signed_headers
)
response.raise_for_status()
except Exception as e:
@ -582,8 +581,9 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
if self.s3_verify is not None
else None
)
# Make the request
response = httpx_client.put(url, data=json_string, headers=signed_headers)
response = httpx_client.put(
prepped.url, data=json_string, headers=signed_headers
)
response.raise_for_status()
except Exception as e:
verbose_logger.exception(f"Error uploading to s3: {str(e)}")
@ -674,8 +674,9 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
# Prepare the signed headers
signed_headers = dict(aws_request.headers.items())
# Make the request
response = await self.async_httpx_client.get(url, headers=signed_headers)
response = await self.async_httpx_client.get(
prepped.url, headers=signed_headers
)
if response.status_code != 200:
verbose_logger.exception(

View file

@ -292,6 +292,50 @@ class TestS3V2UnitTests:
assert result == {"downloaded": "data"}
@patch("asyncio.create_task")
@patch("litellm.integrations.s3_v2.CustomBatchLogger.periodic_flush")
def test_s3_v2_put_url_encodes_spaces_in_object_key(
self, mock_periodic_flush, mock_create_task
):
import requests
from unittest.mock import AsyncMock
from litellm.types.integrations.s3_v2 import s3BatchLoggingElement
mock_periodic_flush.return_value = None
mock_create_task.return_value = None
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.raise_for_status = MagicMock()
s3_object_key = "My Team/2025-09-14/test-key.json"
test_element = s3BatchLoggingElement(
s3_object_key=s3_object_key,
payload={"test": "data"},
s3_object_download_filename="test-file.json",
)
s3_logger = S3Logger(
s3_bucket_name="test-bucket",
s3_endpoint_url="https://s3.amazonaws.com",
s3_aws_access_key_id="test-key",
s3_aws_secret_access_key="test-secret",
s3_region_name="us-east-1",
)
s3_logger.async_httpx_client = AsyncMock()
s3_logger.async_httpx_client.put.return_value = mock_response
asyncio.run(s3_logger.async_upload_data_to_s3(test_element))
call_args = s3_logger.async_httpx_client.put.call_args
assert call_args is not None
actual_url = call_args[0][0]
raw_url = f"https://s3.amazonaws.com/test-bucket/{s3_object_key}"
expected_url = requests.Request("PUT", raw_url).prepare().url
assert actual_url == expected_url
assert " " not in actual_url
@pytest.mark.asyncio
async def test_async_log_event_skips_when_standard_logging_object_missing():
"""