Skip to content

fix(client): follow pagination and verify counts in list getters - #29

Merged
vidiecan merged 2 commits into
dtqfrom
fix/paginate-and-verify-list-getters
Oct 7, 2026
Merged

vidiecan merged 2 commits into
dtqfrom
fix/paginate-and-verify-list-getters

Conversation

@vidiecan

@vidiecan vidiecan commented Oct 6, 2026 •

Copy link
Copy Markdown

Problem

get_bundles(parent=...), get_bitstreams(bundle=...) and get_resourcepolicy() fetched a single page and returned its _embedded list, discarding the HAL page object. get_resourcepolicy did not even pass a size, so it relied on the server default.

Whenever a resource had more elements than one page holds, or the server capped the page size below the requested one, the caller silently got a truncated list. Downstream audits read a truncated policy list as "the file has no such policy" (e.g. a missing Anonymous READ), and nothing could prove completeness after the fact.

Solution

One private helper, _fetch_all_pages, backs the three getters:

  • It requests the first page with the caller's params, then every following page up to the first response's totalPages, reusing the same params (incl. size). The loop is bounded by the first response, so a server repeating a page cannot spin forever.
  • When the response carries a page object, the collected count must equal totalElements. On a mismatch it raises RuntimeError naming the URL and the collected and expected counts.
  • A failure on any later page raises RuntimeError with the HTTP status and URL. It never returns the partial list.

Unchanged

  • A response without a page object behaves as before (single page).
  • The first request is identical; get_resourcepolicy still sends no page param.
  • For bundles and bitstreams, a 404 still returns [] with the INFO log, and other first-page failures still raise RuntimeError.
  • get_resourcepolicy still returns None on a failed first request or an invalid UUID. It still returns [] when _embedded is missing, but now only when totalElements is 0 or absent.
  • The single-uuid get_bundles(uuid=...) path is untouched.

Tests

New cases in tests/test_client_read.py (with a paged() body builder in tests/_helpers.py):

  • Bundles, bitstreams and resource policies each spread over 2 pages: all elements are returned, and the requests carry the expected page/size params.
  • Fewer elements than totalElements (bundles 2 of 3, bitstreams 4 of 5) raise RuntimeError.
  • A 5xx on page 1 raises RuntimeError for all three getters.
  • A single complete page is still exactly one request.
  • For resource policies, a missing _embedded with a non-zero total raises, and with a zero total it returns [].

All existing tests pass unchanged.

get_bundles(parent=...), get_bitstreams(bundle=...) and get_resourcepolicy()
fetched a single page and returned its _embedded list, discarding the HAL
page object. get_resourcepolicy did not even pass a size, so it relied on the
server default. Whenever a resource had more elements than one page holds,
or the server capped the page size below the requested one, the caller got a
silently truncated list. Downstream audits read a truncated policy list as
"the file has no such policy" (eg. a missing Anonymous READ), and nothing
could prove completeness after the fact.

A shared private helper, _fetch_all_pages, now backs the three getters:

- it requests the first page with the caller's params, then every following
  page up to the first response's totalPages, reusing the same params
  (incl. size); the loop is bounded by that first response, so a server
  repeating a page cannot spin forever;
- when the response carries a page object, the collected count must equal
  totalElements (minus the elements before a non-zero start page), otherwise
  it raises RuntimeError naming the url, collected and expected counts;
- a failure on any later page raises RuntimeError with the HTTP status and
  url instead of returning the partial list.

A response without a page object keeps the old single-page behaviour, and the
first request is unchanged (get_resourcepolicy still sends no page param).
All existing contracts stay: 404 -> [] with the INFO log for bundles and
bitstreams, RuntimeError on other first-page failures, get_resourcepolicy's
None on a failed first request or an invalid UUID, and its [] when _embedded
is missing - now only when totalElements is 0 or absent. The single-uuid
get_bundles(uuid=...) path is untouched.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Repeated server pages can satisfy the count check while returning incomplete, duplicated results.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Adds complete HAL pagination and count verification to prevent silently truncated results.

Changes:

  • Introduces a shared paginated-fetch helper.
  • Applies it to bundles, bitstreams, and resource policies.
  • Adds pagination, mismatch, and failure tests.
File Description
dspace_rest_client/​client.py Implements and integrates pagination.
tests/​_helpers.py Adds a paginated HAL response builder.
tests/​test_client_read.py Tests pagination and failure behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread dspace_rest_client/client.py
…ed one

A server that ignores the page parameter and repeats an earlier page can
still make the collected count match totalElements, returning duplicates
and silently missing the real page. Every later page's page.number must
now equal the page that was asked for; otherwise the getter raises.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants