Skip to content

Fix caching and skiptoken handling in paged fetches (fetchAll / fetchMany) - #109

Open
MrRoundRobin wants to merge 2 commits into
diegomvh:mainfrom
MrRoundRobin:mrroundrobin-fix-paged-fetch-cache
Open

MrRoundRobin wants to merge 2 commits into
diegomvh:mainfrom
MrRoundRobin:mrroundrobin-fix-paged-fetch-cache

Conversation

@MrRoundRobin

Copy link
Copy Markdown
Contributor

Fixes two bugs in fetchAll / fetchMany on entity sets, navigation properties and properties.

1. cache-and-network duplicates pages and multiplies requests

With fetchPolicy: 'cache-and-network' and a warm cache, every page emitted twice: once from the cache, then again from the network. expand followed the next link on both emissions, so page N was requested 2^(N-1) times and reduce concatenated the duplicated entities. The first call (cold cache) looked fine; the bug appeared from the second call on.

Fix: all six implementations now use one shared helper, resources/types/paging.ts (fetchPages):

  • Each page gets its own clone of the resource, so concurrent pages no longer mutate a shared query.

  • cache-and-network makes two full passes:

    1. A cache-only pass, emitted only when every page is cached. A cache miss ends this pass silently and does not reach api.errorHandler; this uses an internal opt-out flag in ODataApi.request together with the existing isCacheMiss.
    2. A network-only pass, which requests every page once, stores it, and emits the aggregated result.

    The result is at most 2 emissions, with no duplicates.

  • Other policies keep their per-page behaviour and emit one aggregated result. When the call passes no policy, the API default policy is used.

  • ODataCollection.fetchAll/fetchMany are unchanged; they already reset the collection on every emission.

2. $skiptoken is cut short

ODataEntitiesAnnotations.skiptoken matched /[\d\w\s'\-]+/ on the already decoded nextLink, so the token was cut at the first ,, :, ., % or &. Servers that order by several keys (composite keys, dates) send comma-separated Key-value pairs, so the truncated token requested the wrong page or failed with a 400.

Fix: read $skiptoken (or %24skiptoken) from the raw next link and decode it once. If the value isn't valid percent-encoding, use it as is. The next request encodes it again.

Docs

handbook/caching.md has a new "Paged fetches" section.

Tests

  • New resources/types/paging.spec.ts covers:
    • cache-and-network with a warm, cold or partial cache, including that errorHandler is not called.
    • cache-first, cache-only, and the API default policy.
    • fetchMany, the navigation-property path, collection fetchAll, and composite skiptokens across pages.
  • Without the fix, 6 of the cache-and-network tests fail.
  • annotations.spec.ts has new skiptoken cases.
  • npm test: 874 passed. npm run build: OK.

MrRoundRobin and others added 2 commits October 15, 2026 20:07
fetchAll/fetchMany paged with expand() over fetch(). With cache-and-network
and a warm cache, every page emitted twice (cached, then network), so expand
followed each next link twice: page N was requested 2^(N-1) times and reduce
concatenated duplicated entities.

All six fetchAll/fetchMany implementations now use a shared fetchPages
helper that clones the resource per page. For cache-and-network it makes two
full passes: a cache-only pass, emitted only when every page is cached and
whose cache misses skip the api errorHandler, then a network-only pass.
Other policies keep their per-page behaviour and emit one result.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
ODataEntitiesAnnotations.skiptoken matched /[\d\w\s'\-]+/ on the already
decoded nextLink, so the token was cut at the first ',', ':', '.', '%' or
'&'. Servers that order by several keys (composite keys, dates) send one
Key-value pair per sort key, comma separated; the truncated token made
fetchAll/fetchMany request a wrong page or fail with 400.

Read the option from the raw next link and decode it once, falling back to
the raw value when it is not valid percent-encoding. The next request
encodes it again.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

1 participant