skip to content

How does OData 4.01 server-driven paging work, and what must a client do with the next link and the maxpagesize preference?

level: middleimportance: should knowfreq 21%

answer

  1. the service chooses to split
  2. follow until no link remains
  3. the link is opaque
  4. a preference, not a command

basics

~20 s

An OData 4.01 service may return part of a collection with a next link; the client follows it unchanged until a page has none. Prefer: maxpagesize=N only requests a page size — the service may apply it or choose another.

solid answer

~40 s

In **server-driven paging** the service, not the client, decides to return a partial collection. Such a response MUST contain a next link (`@nextLink` in 4.01 JSON, `@odata.nextLink` in 4.0), and the final page MUST NOT. The client MUST treat the link as opaque and MUST NOT append system query options to it; the service may encode its position in `$skiptoken`, which clients MUST NOT construct themselves. A client asks for a page size with `Prefer: maxpagesize=N` (`odata.maxpagesize` in 4.0); the service may honour it, pick a different size, and MAY report the applied value in `Preference-Applied`. Paging applies to every collection in the response, so an expanded `Orders` can carry its own next link. `$top` is different: it is the client's limit on the total, which the service may still deliver over several pages.

code

http · 14 lines
http
GET /odata/Customers?$filter=City%20eq%20'Lyon' HTTP/1.1
Host: api.example.com
Prefer: maxpagesize=50

HTTP/1.1 200 OK
Content-Type: application/json
OData-Version: 4.01
Preference-Applied: maxpagesize=25

{
  "@context": "https://api.example.com/odata/$metadata#Customers",
  "value": [ { "ID": "C001", "Name": "Arnaud Fils", "City": "Lyon" } ],
  "@nextLink": "https://api.example.com/odata/Customers?$filter=City%20eq%20'Lyon'&$skiptoken=QzAyNQ"
}

go deeper

for a junior

Recall that a partial response carries a next link, and that the client keeps following it until a page arrives without one.

for a middle

Explain the opaque next link, the rule against appending options or using $skiptoken, and how maxpagesize differs from $top.

for a senior

Show that you would handle Preference-Applied, nested next links on expanded collections and page sizes smaller than requested in a real client.

for a principal

Discuss how a service should pick its page size and token lifetime, balancing payload size, database cost and the round trips a small page forces on clients.

## Two kinds of paging OData has two independent mechanisms, and confusing them is the most common mistake: | | Client-driven | Server-driven | |---|---|---| | Who decides | the client, with `$top` and `$skip` | the service | | What the client sends | positions to skip and a count to keep | optionally `Prefer: maxpagesize=N` | | What signals more data | nothing; the client computes the next request | a next link in the response | | Purpose | choose which slice of the result to show | protect the service and the client from huge responses | In the 4.01 evaluation order, `$skip` and `$top` are applied **before** server-driven paging. So `$top=1000` limits the result to 1,000 items, and the service may still deliver those 1,000 over several pages. ## The rules in the specification The OData 4.01 Protocol (section 11.2.6.7) is short and strict: - A response that includes only a partial set of the items identified by the request URL **MUST** contain a next link; the final partial set **MUST NOT** contain one. - The client **MUST** treat the next link URL as opaque and **MUST NOT** append system query options to it. - Services may build next links with the reserved option `$skiptoken`, whose content is opaque and service-specific. Clients **MUST NOT** use `$skiptoken` when constructing requests. - A service may refuse a change of format on later pages, so the client **SHOULD** request the same format with a compatible `Accept` header when following the link. The page size itself is a service decision. The specification does not define a default page size; any number you have seen is an implementation's choice. ## The maxpagesize preference A client can say what it can handle, through the HTTP `Prefer` header: 1. The client sends `Prefer: maxpagesize=50`, asking that **each collection** in the response hold at most 50 items. 2. If a collection holds more, it SHOULD be a partial set with a next link. 3. The service may apply the requested size, a different one, or a size of its own when no preference is sent. 4. If it limits collections and the client sent the preference, it MAY return `Preference-Applied: maxpagesize=…` with the size actually applied, which may differ from the request. 5. The client MAY send a different value with every request that follows a next link. **Versions:** 4.0 named the preference `odata.maxpagesize`. 4.01 services that support `maxpagesize` SHOULD also accept `odata.maxpagesize`, clients SHOULD send `odata.maxpagesize` to stay compatible with 4.0 services, and if both are sent the `maxpagesize` value SHOULD win. In JSON, 4.0 writes `@odata.nextLink`; 4.01 writes `@nextLink`. ## What the exchange looks like ```http GET /odata/Customers?$filter=City eq 'Lyon'&$count=true Prefer: maxpagesize=50 HTTP/1.1 200 OK Preference-Applied: maxpagesize=25 OData-Version: 4.01 { "@count": 57, "value": [ ...25 customers... ], "@nextLink": "https://api.example.com/odata/Customers?$filter=City%20eq%20'Lyon'&$count=true&$skiptoken=QzAyNQ" } ``` The service capped pages at 25 although the client asked for 50. The client requests the `@nextLink` URL exactly as given, receives the next 25 with another next link, then a final page of seven with none. ## Expanded collections page too The preference applies to every collection in the result. With `$expand=Orders`, the protocol's own example notes that a page should carry a next link for the customers if there are more than the maximum, and additional next links for each `Orders` collection that exceeds it — written `Orders@nextLink` on the customer. A client that reads only the top-level link silently loses orders. ## A robust client loop 1. Send the first request with the query options the screen needs, and optionally a `maxpagesize` preference. 2. Read the items in `value`, plus any nested next links on expanded collections that the screen needs in full. 3. If the response has a next link, request that exact URL with the same `Accept` format; otherwise stop. 4. Treat a page smaller than requested as normal, not as the end. ## Client bugs this catches - **Building pages itself** from `$skip` while the service is paging, which double-reads or misses rows. - **Editing the next link** — appending `$select` or `$top` — which the specification forbids; the link already encodes the query. - **Assuming the page size**, for example treating a page of 25 as the end because the client asked for 50. The end is signalled only by the absence of a next link. - **Ignoring nested next links** on expanded collections. The count, if requested, is the total across all pages, so it is useful for a progress indicator but not as a loop condition.

  • An OData client builds its next request by appending `&$top=50` to the next link it received. What is wrong with that?
    The OData 4.01 protocol says clients MUST treat the next link as opaque and MUST NOT append system query options to it; the link already encodes the original query and the position, and a second `$top` would conflict with it. To change the page size, the client sends a different `maxpagesize` preference with the request that follows the link, which the specification explicitly allows.
  • Must an OData 4.01 service honour `Prefer: maxpagesize=500`?
    No. The preference is a request: the service may apply that size, apply a different one, or page by its own rule. If it limits collections and the client sent the preference, it MAY include `Preference-Applied` with the size it used. Clients must therefore handle any page size and stop only when a page arrives without a next link.

saying these in an interview costs you the question

  • The OData specification sets a default page size for every service
  • The client can build the next page itself by sending $skiptoken
  • Prefer: maxpagesize obliges the service to return exactly that many items
  • Only the top-level collection is paged; expanded collections always arrive whole
  • Appending $filter to a next link safely narrows the remaining pages