fix(s3_v2): use prepared URL for SigV4-signed S3 requests#25074
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR fixes a SigV4 signature mismatch on S3 PUT/GET requests when the object key contains characters that need percent-encoding (e.g. spaces in team-name prefixes). Previously, SigV4 was computed over the percent-encoded
Confidence Score: 5/5Safe to merge — the bug fix is correct and all remaining findings are minor style issues. The core fix is correct and consistent across all three affected call sites. No logic, security, or data-integrity issues were found. All remaining comments (inline imports, missing blank line, incomplete test coverage of sync/download paths) are P2 style and quality suggestions that do not block correctness. No files require special attention; tests/test_litellm/integrations/test_s3_v2.py has minor style issues with inline imports.
|
| Filename | Overview |
|---|---|
| litellm/integrations/s3_v2.py | Replaces raw url with prepped.url (percent-encoded) in three HTTP call sites (async PUT, sync PUT, async GET), ensuring the URL sent to httpx matches the URL that was SigV4-signed — a correct and minimal fix. |
| tests/test_litellm/integrations/test_s3_v2.py | Adds a mock-only unit test verifying the async upload URL-encodes spaces; inline imports and a missing blank line are minor style issues, and sync/download paths lack analogous coverage. |
Sequence Diagram
sequenceDiagram
participant S3Logger
participant Prep as requests.prepare()
participant SigV4
participant HTTPX
S3Logger->>Prep: prepare(method, raw_url)
Prep-->>S3Logger: prepped.url (percent-encoded)
S3Logger->>SigV4: sign(prepped.url)
SigV4-->>S3Logger: signed_headers
S3Logger->>HTTPX: request(prepped.url, signed_headers)
Note over HTTPX: URL matches signed URL
Reviews (1): Last reviewed commit: "fix(s3_v2): use prepared URL for SigV4-s..." | Re-trigger Greptile
| import requests | ||
| from unittest.mock import AsyncMock | ||
|
|
||
| from litellm.types.integrations.s3_v2 import s3BatchLoggingElement |
There was a problem hiding this comment.
Inline imports should be moved to module level
Per the project's style guide (CLAUDE.md), imports inside methods make dependencies harder to trace and hurt readability. requests, AsyncMock, and s3BatchLoggingElement should be declared at the top of the file alongside the existing imports.
The top of the file already imports MagicMock and patch from unittest.mock; AsyncMock can simply be added to that same import:
| import requests | |
| from unittest.mock import AsyncMock | |
| from litellm.types.integrations.s3_v2 import s3BatchLoggingElement | |
| import asyncio | |
| from datetime import datetime | |
| from unittest.mock import AsyncMock, MagicMock, patch | |
| import pytest | |
| import requests | |
| from litellm.integrations.s3_v2 import S3Logger | |
| from litellm.types.integrations.s3_v2 import s3BatchLoggingElement | |
| from litellm.types.utils import StandardLoggingPayload |
Then remove the three import lines from inside the test method body.
Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
|
||
| @pytest.mark.asyncio |
There was a problem hiding this comment.
Missing blank line between class and module-level function
PEP 8 requires two blank lines before a top-level function definition. There is currently only one blank line between the end of TestS3V2UnitTests and the @pytest.mark.asyncio decorator.
| @pytest.mark.asyncio | |
| @pytest.mark.asyncio |
| 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 |
There was a problem hiding this comment.
Test covers async upload only — sync upload and download paths not tested
The fix applies prepped.url to three code paths: async_upload_data_to_s3, upload_data_to_s3 (sync), and _download_object_from_s3. Only the async upload path has a corresponding test for the URL-encoding behaviour. Consider adding parallel tests for the sync upload and the download path to ensure parity and guard against future regressions on those paths.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
d6351a3
into
BerriAI:litellm_oss_staging_04_04_2026
fixes #25019
Summary
Changes
Type