Skip to content

Commit dbabf5d

Browse files
authored
Merge pull request #19 from dataquest-dev/fix/explicit-fetch-errors
fix(client): raise informative error on non-404 fetch failure
2 parents 3273055 + 4049e94 commit dbabf5d

2 files changed

Lines changed: 39 additions & 17 deletions

File tree

dspace_rest_client/client.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -733,12 +733,17 @@ def get_bundles(self, parent=None, uuid=None, page=0, size=20, sort=None):
733733
if sort is not None:
734734
params['sort'] = sort
735735
r_json = self.fetch_resource(url, params=params)
736-
if r_json is None and getattr(self._last_err, 'status_code', None) == 404:
737-
# the item (or bundle) no longer exists - a deleted item simply has
738-
# no bundles, which is a clean empty result, not a crash. any other
739-
# failure falls through and still surfaces to the caller.
740-
_logger.info(f'No bundles: resource not found (404) [{url}]')
741-
return bundles
736+
if r_json is None:
737+
status = getattr(self._last_err, 'status_code', None)
738+
if status == 404:
739+
# a deleted item (or bundle) simply has no bundles, which is a
740+
# clean empty result, not a crash.
741+
_logger.info(f'No bundles: resource not found (404) [{url}]')
742+
return bundles
743+
# any other failure surfaces with its status + url, not as an opaque
744+
# 'NoneType is not subscriptable' further down, so the caller can
745+
# see what failed and retry.
746+
raise RuntimeError(f'Failed to fetch bundles: HTTP {status} [{url}]')
742747
try:
743748
if single_result:
744749
bundles.append(Bundle(r_json))
@@ -800,13 +805,16 @@ def get_bitstreams(self, uuid=None, bundle=None, page=0, size=20, sort=None):
800805
if sort is not None:
801806
params['sort'] = sort
802807
r_json = self.fetch_resource(url, params=params)
803-
if r_json is None and getattr(self._last_err, 'status_code', None) == 404:
804-
# the bundle (or item) is gone - no bitstreams, a clean empty result
805-
# rather than a crash. Mirrors get_bundles (#16). Any other failure
806-
# (a transient 5xx, say) falls through and still surfaces to the
807-
# caller so it is retried, not silently recorded as "no bitstreams".
808-
_logger.info(f'No bitstreams: resource not found (404) [{url}]')
809-
return list()
808+
if r_json is None:
809+
status = getattr(self._last_err, 'status_code', None)
810+
if status == 404:
811+
# the bundle (or item) is gone - no bitstreams, a clean empty
812+
# result rather than a crash. Mirrors get_bundles.
813+
_logger.info(f'No bitstreams: resource not found (404) [{url}]')
814+
return list()
815+
# a transient 5xx must NOT masquerade as "no bitstreams"; surface it
816+
# with status + url so the caller can retry, not an opaque TypeError.
817+
raise RuntimeError(f'Failed to fetch bitstreams: HTTP {status} [{url}]')
810818
bitstreams = list()
811819
if '_embedded' in r_json and 'bitstreams' in r_json['_embedded']:
812820
for bitstream_resource in r_json['_embedded']['bitstreams']:

tests/test_client_read.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ def test_deleted_item_404_returns_empty_list(self):
128128
json={"timestamp": "2026-01-01"})
129129
self.assertEqual(c.get_bundles(parent=parent), [])
130130

131+
def test_non_404_error_raises_informative_error(self):
132+
# a non-404 fetch failure surfaces with its status + url so the caller
133+
# can retry, rather than an opaque 'NoneType is not subscriptable'.
134+
c = make_client()
135+
parent = Item(item_json(ITEM_UUID))
136+
with requests_mock.Mocker() as m:
137+
m.get(f"{API}/core/items/{ITEM_UUID}/bundles",
138+
status_code=500, text="boom")
139+
with self.assertRaises(RuntimeError) as ctx:
140+
c.get_bundles(parent=parent)
141+
self.assertIn("500", str(ctx.exception))
142+
131143
def test_no_args_returns_empty_without_request(self):
132144
c = make_client()
133145
with requests_mock.Mocker() as m:
@@ -181,16 +193,18 @@ def test_deleted_bundle_404_returns_empty_list(self):
181193
status_code=404, json={"timestamp": "2026-01-01"})
182194
self.assertEqual(c.get_bitstreams(bundle=bundle), [])
183195

184-
def test_non_404_error_still_surfaces(self):
185-
# a transient 5xx must NOT masquerade as "no bitstreams"; it surfaces so
186-
# the caller can retry, exactly as get_bundles does for non-404 errors.
196+
def test_non_404_error_raises_informative_error(self):
197+
# a transient 5xx must NOT masquerade as "no bitstreams"; it surfaces
198+
# with its status + url (not a bare TypeError) so the caller can retry.
187199
c = make_client()
188200
bundle = Bundle(bundle_json("bnd2"))
189201
with requests_mock.Mocker() as m:
190202
m.get(f"{API}/core/bundles/bnd2/bitstreams",
191203
status_code=500, text="boom")
192-
with self.assertRaises(Exception):
204+
with self.assertRaises(RuntimeError) as ctx:
193205
c.get_bitstreams(bundle=bundle)
206+
self.assertIn("500", str(ctx.exception))
207+
self.assertIn("/core/bundles/bnd2/bitstreams", str(ctx.exception))
194208

195209
def test_200_without_bitstreams_returns_empty_list(self):
196210
# a well-formed response with no bitstreams -> [] (not None), so callers

0 commit comments

Comments
 (0)