Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pagination

A paginated endpoint is an ordinary body endpoint whose response type knows how to ask for the next page. You get two consumption modes for free: query one page like any endpoint, or call .paged() for an auto-fetching stream over all of them.

Two ways to consume

// One page — a normal query. You handle next_page tokens yourself.
let page = api.query(ListShipments::new()).await?;

// All pages — an auto-fetching StreamIterator.
let mut all = api.query(ListShipments::new().paged()).await?;
let everything: Vec<_> = all.try_collect().await?;

.paged() — the IntoPaged trait, blanket-implemented for every endpoint whose output paginates — turns the endpoint into a Paged<T> wrapper whose Output is a PagedIterator<P>, a StreamIterator handle, so it consumes with the same next() / try_next() / try_collect() / take() you already know. It refetches the next page automatically when the current buffer drains. The bounds say what qualifies: T: Endpoint + Clone + DecodeBody, T::Output: PaginatedResponse<T>, and a response-shaped lane (Requires: Capability<Response = Response>).

Implementing PaginatedResponse

To make a response paginate, implement PaginatedResponse<Endpoint> on the output type — three required methods, plus a defaulted total() for an API that reports one:

impl PaginatedResponse<ListShipments> for ListShipmentsOutput {
    type Item = Shipment;
    type Metadata = ();
    fn items(&mut self) -> Vec<Self::Item> {
        core::mem::take(&mut self.shipments)          // hand off this page's items
    }
    fn next_page(&self, current: &ListShipments) -> Option<ListShipments> {
        self.next_cursor.clone().map(|c| {            // None ends the stream
            let mut next = current.clone();
            next.cursor = Some(c);
            next
        })
    }
    fn metadata(self) -> Self::Metadata {}            // whatever is neither an item nor continuation state
}

metadata(self) hands out everything on the page that is neither an item nor continuation state — a total count, a rate-limit snapshot — and the iterator exposes it through metadata(); () when there is none.

The load-bearing detail is the signature: next_page(&self, current: &E) -> Option<E> takes the current endpoint and returns the next one (or None to stop). It gets the current endpoint because the next request is almost always the same query with one field changed — a cursor, an offset, a page number.

No cycle guard. A next_page that never returns None — a server cursor that doesn’t advance — produces an infinite stream. Make sure your stop condition actually triggers.

The next_page body is where the pagination scheme lives, and it’s just Rust:

  • Cursor / continuation token (the common case) — copy the response’s next_cursor / continuation_token into the next request. grok’s custom_voices and S3’s ListObjectsV2 both work this way.
  • Offset / page number — increment an offset or page field until a page comes back short or empty.
  • RFC 5988 Link header — parse rel="next" from the Link header and follow the absolute URL (via an absolute-URL endpoint). capi_base_decoders::LinkHeader is the parser: LinkHeader::of(&headers).next(&base) (or .rel("next", &base)) resolves the relation against the request’s URL and hands back a UrlBuilder.
  • A continuation in the response headers — Azure Tables carries its cursor in x-ms-continuation-NextPartitionKey; next_page sees only the decoded body, so the body decoder lifts the header values onto the page type first, and next_page reads them from there.

Custom decoding: DecodePaged

PaginatedResponse covers the common case where the page is a plain decoded body and the continuation lives in it — Paged<T> already snapshots the endpoint as its State and hands it to next_page as current. Reach for DecodePaged when the page needs the response headdecode_paged receives it beside the body, so a header-driven continuation needs no lift onto the body type — or when the page is not the plain decoded output: a custom Page type, or a different NextPage. Like every decoder it has a state() snapshot, captured before body() consumes the endpoint and fed into decode_paged(...), which builds the next-page endpoint. A DecodePagedSimple variant covers the straightforward subset without the full ceremony.

Consuming a Paged<T>

Because a Paged<T> yields a StreamIterator, the whole streaming vocabulary applies: try_next() for a page-lazy loop, try_collect() to gather everything, take(n) to cap the fetch, and size_hint() (backed by the response’s total() when the API reports one). Items stream out as pages arrive — you never wait for the last page to start processing the first.

Every follow-up page rides the detached FollowUpChannel the decoder carried out of the first query, so it runs the same traversal: the config’s members, the seal, the request budget, the recorder, and the endpoint’s rate_limit — which is re-resolved per send, so a shared limiter paces the pages too. A wire error on a later page arrives inside QueryError as a boxed client error, since the concrete client type is behind the channel. And a derived query input can target the paged flow directly: an IntoEndpoint whose type Endpoint = Paged<ListShipments> and into_endpoint ends in .paged(), with Remap = remap::Yes, maps the PagedIterator lazily across every follow-up page (consuming and patching).

Next: Switching Outputs, where one endpoint yields one of several output types.