Skip to content

Commit ddb5054

Browse files
authored
fix(storage): catch bad responses from server (#1344)
1 parent c1e7986 commit ddb5054

File tree

3 files changed

+68
-13
lines changed

3 files changed

+68
-13
lines changed

src/storage/src/storage3/_async/file_api.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,14 @@ async def _request(
7777
)
7878
response.raise_for_status()
7979
except HTTPStatusError as exc:
80-
resp = exc.response.json()
81-
raise StorageApiError(
82-
resp["message"], resp["error"], resp["statusCode"]
83-
) from exc
80+
try:
81+
resp = exc.response.json()
82+
raise StorageApiError(
83+
resp["message"], resp["error"], resp["statusCode"]
84+
) from exc
85+
except KeyError as err:
86+
message = f"Unable to parse error message: {resp.text}"
87+
raise StorageApiError(message, "InternalError", 400) from err
8488

8589
# close the resource before returning the response
8690
if files and "file" in files and isinstance(files["file"][1], BufferedReader):

src/storage/src/storage3/_sync/file_api.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dataclasses import dataclass, field
77
from io import BufferedReader, FileIO
88
from pathlib import Path
9-
from typing import Any, List, Literal, Optional, Union, cast
9+
from typing import Any, Dict, List, Literal, Optional, Union, cast
1010

1111
from httpx import Client, Headers, HTTPStatusError, Response
1212
from yarl import URL
@@ -77,10 +77,14 @@ def _request(
7777
)
7878
response.raise_for_status()
7979
except HTTPStatusError as exc:
80-
resp = exc.response.json()
81-
raise StorageApiError(
82-
resp["message"], resp["error"], resp["statusCode"]
83-
) from exc
80+
try:
81+
resp = exc.response.json()
82+
raise StorageApiError(
83+
resp["message"], resp["error"], resp["statusCode"]
84+
) from exc
85+
except KeyError as err:
86+
message = f"Unable to parse error message: {resp.text}"
87+
raise StorageApiError(message, "InternalError", 400) from err
8488

8589
# close the resource before returning the response
8690
if files and "file" in files and isinstance(files["file"][1], BufferedReader):
@@ -436,7 +440,12 @@ def list(
436440
)
437441
return response.json()
438442

439-
def download(self, path: str, options: Optional[DownloadOptions] = None) -> bytes:
443+
def download(
444+
self,
445+
path: str,
446+
options: Optional[DownloadOptions] = None,
447+
query_params: Optional[Dict[str, str]] = None,
448+
) -> bytes:
440449
"""
441450
Downloads a file.
442451
@@ -445,20 +454,23 @@ def download(self, path: str, options: Optional[DownloadOptions] = None) -> byte
445454
path
446455
The file path to be downloaded, including the path and file name. For example `folder/image.png`.
447456
"""
448-
url_options = options or {}
457+
url_options = options or DownloadOptions()
449458
render_path = (
450459
["render", "image", "authenticated"]
451460
if url_options.get("transform")
452461
else ["object"]
453462
)
454463

455-
transform_options = url_options.get("transform") or {}
464+
transform_options = url_options.get("transform") or TransformOptions()
456465

457466
path_parts = relative_path_to_parts(path)
458467
response = self._request(
459468
"GET",
460469
[*render_path, self.id, *path_parts],
461-
query_params=transform_to_dict(transform_options),
470+
query_params={
471+
**transform_to_dict(transform_options),
472+
**(query_params or {}),
473+
},
462474
)
463475
return response.content
464476

src/storage/tests/_sync/test_client.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,45 @@ def test_client_upload(
283283
assert image_info.get("metadata", {}).get("mimetype") == file.mime_type
284284

285285

286+
def test_client_upload_with_query(
287+
storage_file_client: SyncBucketProxy, file: FileForTesting
288+
) -> None:
289+
"""Ensure we can upload files to a bucket, even with query parameters"""
290+
storage_file_client.upload(
291+
file.bucket_path, file.local_path, {"content-type": file.mime_type}
292+
)
293+
294+
image = storage_file_client.download(
295+
file.bucket_path, query_params={"my-param": "test"}
296+
)
297+
files = storage_file_client.list(file.bucket_folder)
298+
image_info = next((f for f in files if f.get("name") == file.name), None)
299+
300+
assert image == file.file_content
301+
assert image_info is not None
302+
assert image_info.get("metadata", {}).get("mimetype") == file.mime_type
303+
304+
305+
def test_client_download_with_query_doesnt_lose_params(
306+
storage_file_client: SyncBucketProxy, file: FileForTesting
307+
) -> None:
308+
"""Ensure query params aren't lost"""
309+
from yarl import URL
310+
311+
params = {"my-param": "test"}
312+
mock_response = Mock()
313+
with patch.object(HttpxClient, "request") as mock_request:
314+
mock_request.return_value = mock_response
315+
storage_file_client.download(file.bucket_path, query_params=params)
316+
expected_url = storage_file_client._base_url.joinpath(
317+
"object", storage_file_client.id, *URL(file.bucket_path).parts
318+
).with_query(params)
319+
actual_url = mock_request.call_args[0][1]
320+
321+
assert URL(actual_url).query == params
322+
assert str(expected_url) == actual_url
323+
324+
286325
def test_client_update(
287326
storage_file_client: SyncBucketProxy,
288327
two_files: list[FileForTesting],

0 commit comments

Comments
 (0)