Skip to content

feat(#1346): actually paginate the ownership chain history response - #1424

Closed
jarik2014 wants to merge 2 commits into
CodeGirlsInc:mainfrom
jarik2014:fix/1346-ownership-chain-pagination
Closed

jarik2014 wants to merge 2 commits into
CodeGirlsInc:mainfrom
jarik2014:fix/1346-ownership-chain-pagination

Conversation

@jarik2014

@jarik2014 jarik2014 commented Sep 26, 2026 •

Copy link
Copy Markdown

Closes #1346.

What the issue actually is

module/ownership_chain/pagination.rs is already in main — it was added in #1401 — but nothing calls it: it is not declared in its own mod.rs, its Page<T> type is not serializable, and no handler uses it. Dead code. That is why the endpoint still returns a whole ownership chain in one response, and it is why fixing this needed no new pagination logic, only wiring and a hardening pass.

The change

  • declares the module and makes Page<T> serializable;
  • hardens paginate: a cursor past the end is an empty page rather than a panic on items[cursor..end], and a page size of zero can no longer return a cursor that never advances (an infinite loop for a client that follows cursors);
  • moves the page-size policy into the pagination module — default 50, cap 200 — so both history handlers share it instead of one of them owning the constants;
  • wires the endpoint: GET /verify/:hash/history?cursor=&page_size= serves a bounded page and the response carries total and next_cursor, so a client can walk a long chain to the end. count keeps its old meaning of "records in this response", which is now the page rather than the whole chain.

Verified

$ cargo test --test handler_integration_tests history
test test_history_endpoint_serves_bounded_pages ... ok        # page 1 (2 of 5) -> cursor 2; page 2 -> cursor 4; final page -> null
test test_history_endpoint_clamps_a_greedy_page_size ... ok   # ?page_size=10000 over 300 records -> 200 + cursor 200
test test_history_endpoint_without_query_returns_the_first_page ... ok  # 60 records -> 50 + cursor 50
test test_verify_history_path_with_invalid_hash_returns_400 ... ok
test result: ok. 4 passed; 0 failed
$ cargo test --lib pagination
test result: ok. 7 passed; 0 failed    # first/middle/last page, remainder, cursor past the end, zero page size, empty chain

Whole suite on this branch (all targets, which is itself new — see below):

$ cargo test --no-fail-fast
lib                            142 passed; 11 failed
main                             0 passed;  0 failed
graceful_shutdown                2 passed;  0 failed
handler_integration_tests       17 passed;  0 failed
hash_validation                  6 passed;  0 failed
rate_limit                       3 passed;  0 failed
revoke                           4 passed;  1 failed
doc-tests                        1 passed;  0 failed

The 12 failures are pre-existing test-code failures in modules this PR does not touch (hash_validator non-ASCII cases, stellar httpmock cases, webhook, and one in tests/revoke.rs). They were unreachable until now because the crate did not compile at all — see the first commit, which is a prerequisite rather than part of this issue.

The first commit is a compile fix, not part of the issue

cargo check and cargo test both fail on main before a single test runs, and the test targets had never been compiled. Fixing it is what makes any test of this issue possible, so it is split out and reviewable on its own: chrono was pulled in without its serde feature while Event derives serde over DateTime<Utc>; AppState derives Clone while holding a governor RateLimiter, which is not Clone (now behind an Arc); tower was declared without util, so no ServiceExt::oneshot in the integration tests resolved; three test files construct AppState and needed the same Arc; and src/stellar.rs's mock borrowed data_key after moving it into a json! body.

One thing worth knowing before you review

This repository has two app() functions and two copies of verify_document_history: one pair in src/lib.rs, another in src/routes.rs + src/handlers/verify.rs. The binary and the tests use the lib.rs one (main.rs imports stellar_doc_verifier::app), and I found that out the hard way: paginating only the handlers/ copy left the endpoint's behaviour unchanged, because the route table that actually serves traffic points at the other one. Both copies are kept in step here, including the shared HistoryQuery type, but consolidating the duplicates is a separate job I did not want to fold into this PR.

Could you assign this issue to me in Drips? The wave only credits the assigned applicant, and I have applied for it.


About the red Vercel check: it fails with "Authorization required to deploy" — Vercel will not deploy a fork without the maintainer authorising it, so that check cannot pass from here and is unrelated to this change. What did pass on this PR: the Rust build and test jobs for the contract crate.

Neither `cargo check` nor `cargo test` gets anywhere on main - the library fails
first, and the test targets have never been compiled at all (verified with the
pinned 1.89.0 toolchain).

Library errors:

- src/event.rs - `Event` derives Serialize/Deserialize over `DateTime<Utc>`, but
  chrono was pulled in without its `serde` feature, so those bounds were never
  satisfiable;
- src/lib.rs - `AppState` derives `Clone` while holding a governor
  `RateLimiter`, which is not `Clone`, so every handler failed the bound.

Test targets, which only surfaced once the library compiled:

- `tower` was declared without the `util` feature, so every
  `ServiceExt::oneshot` in tests/handler_integration_tests.rs failed to resolve;
- tests/{hash_validation,rate_limit,revoke}.rs construct an AppState, so they
  need the same `Arc::new` around the limiter;
- src/stellar.rs' mock put `data_key` into a `json!` body and then asserted on it
  again, borrowing after a move.

Verified: `cargo test --no-run` compiles every target, and
`cargo test --no-fail-fast` now runs the whole suite. 12 tests fail there
(the library target and tests/revoke.rs) - all of them pre-existing failures in
test code that was unreachable until now, none of them touched here.
…y response

The issue is that a document with a long history is served in one unbounded
response. The module meant to prevent that already exists in main
(`module/ownership_chain/pagination.rs`) but nothing calls it: it is not
declared in its own mod.rs, its `Page` type is not serializable, and no handler
uses it - dead code, which is why the endpoint still returns the whole chain.

What this does:

- declares the module and makes `Page<T>` serializable;
- hardens `paginate`: a cursor past the end is an empty page rather than a
  panic, and a page size of zero cannot return a cursor that never advances;
- moves the page-size limits into the pagination module (default 50, cap 200) so
  both history handlers share them instead of one of them owning them;
- wires the endpoint: `GET /verify/:hash/history?cursor=&page_size=` now serves
  a bounded page, and the response carries `total` and `next_cursor` so a client
  can walk the chain to the end.

Note for the reviewer: this repository has two `app()` functions and two copies
of `verify_document_history` (src/lib.rs and src/{routes,handlers/verify}.rs).
The one the binary and the tests actually use is src/lib.rs' - I found that the
hard way, because paginating only the handlers/ copy left the endpoint's
behaviour unchanged. Both copies are kept in step here; consolidating them is a
separate job.

Tests: 7 unit tests over the `paginate` boundaries (first/middle/last page, a
page that covers the remainder, a cursor past the end, a zero page size, an
empty chain) and 3 endpoint tests over a seeded 5-record chain (page 1 then page
2 then the final page with a null cursor), a 300-record chain with
`page_size=10000` (clamped to 200 server-side), and no query string at all
(the documented default).
@vercel

vercel Bot commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

@jarik2014 is attempting to deploy a commit to the Mftee's projects Team on Vercel.

A member of the Team first needs to authorize it.

@jarik2014

Copy link
Copy Markdown
Author

Issue has been completed by another contributor (@lynaDev2) and points awarded. Closing this PR as redundant.

@jarik2014 jarik2014 closed this Sep 28, 2026
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.

Ownership chain lookups are not paginated

1 participant