DEV Community

FlareCanary
FlareCanary

Posted on

Contentful's August 14 CMA update silently truncates your role lists and strips fields from user responses

If you manage Contentful spaces programmatically — a script that syncs roles into your IdP, a member-roster export, an onboarding job that provisions access, anything that hits the Content Management API to enumerate roles or users — two changes land on August 14, 2026 that fail in the worst way: they return a 200, valid JSON, and less data than they did the day before.

Both are listed on Contentful's own API changes page. Neither throws an error. Both depend on a precondition you might not be checking — the shape of the pagination block, or the role of the token doing the reading.

Change 1: the roles endpoint swaps offset pagination for cursors

The endpoint is GET /spaces/{spaceId}/roles. Today it paginates the way most Contentful collection endpoints do:

{
  "total": 42,
  "skip": 0,
  "limit": 25,
  "items": [ ... ]
}
Enter fullscreen mode Exit fullscreen mode

As of August 14 it switches to cursor-based pagination. total and skip go away; you get a pages object with next/previous cursor links instead, and limit stays:

{
  "limit": 25,
  "pages": { "next": "<cursor>", "previous": null },
  "items": [ ... ]
}
Enter fullscreen mode Exit fullscreen mode

The migration is documented and the fix is "use pageNext/pagePrev cursor parameters instead of skip." The problem is what happens to code that doesn't migrate, because none of it errors out.

The three ways this silently truncates

1. The total-driven loop exits after page one. This is the canonical offset-pagination idiom:

let skip = 0;
const all = [];
do {
  const res = await getRoles({ skip, limit: 25 });
  all.push(...res.items);
  skip += res.limit;
} while (skip < res.total);   // res.total is now undefined
Enter fullscreen mode Exit fullscreen mode

After August 14, res.total is undefined. skip < undefined evaluates to false on the first iteration, so the loop runs exactly once and stops. A space with 60 roles syncs the first 25 and silently drops 35 — no exception, no empty result you'd notice, just a short list that looks plausible.

2. The "short page means last page" heuristic drops the tail. Code that decides it's done when items.length < limit is making an assumption cursor pagination doesn't honor — a non-final cursor page is not guaranteed to be full. Stop on the first short page and you lose every role after it. (This is the same class of bug teams hit with cursor migrations on Jira, monday.com, and Greenhouse — a server can return a half-full page with a live next cursor.)

3. Persisted numeric offsets have nothing to persist. A cron job that stores "I got through skip=50, resume there next run" breaks completely: cursors are opaque tokens scoped to a live pagination session, not a stable integer offset you can stash in a database and reuse tomorrow. The stored offset is meaningless and the resume logic either errors or restarts from the top.

What runs GET /roles? Access-provisioning automation that maps Contentful roles to an IdP, compliance exports that enumerate who-can-do-what, and admin dashboards. A truncated role list means access reviews that miss roles, and provisioning logic that can't find a role it's supposed to assign.

Change 2: user-data responses become role-based

The second change hits "Get all users in a space" and "Get all Space Members in a space." Contentful's exact wording:

API responses for user data will become role-based: Admin users will continue to receive full responses, with no changes. Non-admin users will receive limited user information.

So the same request, same endpoint, same 200 returns a fuller or thinner user object depending on the role of the token making the call. Admin token: unchanged. Non-admin token: trimmed payload.

This is a nastier silent surface than the pagination one, because it's environment-dependent in a way that defeats casual testing:

1. It works in dev and under-fetches in prod. You build and test the integration with your own admin Personal Access Token — full responses, everything's there. It ships to prod running under a scoped, non-admin service token, and after August 14 the fields you depend on quietly stop coming back. Classic "works on my machine," except the machine is the token's role and nothing in your code changed.

2. User-sync and audit exports write nulls. A job that reads email (or any of the fields that get trimmed) off the users list with a non-admin token now gets undefined for them. Downstream you get user records synced with blank emails, SCIM-ish reconciliation that can't match people, audit CSVs with empty columns — all populated from a 200 response that looks successful.

3. Member dashboards render partial detail with nothing to alert on. A 200 with a smaller object doesn't trip error monitoring. The dashboard just shows less, and unless someone eyeballs it against yesterday, the regression is invisible.

The fix is to check, before August 14, which token role your integration uses against these endpoints. If it's non-admin, confirm exactly which user fields you read, and either elevate the token to admin (if the use case justifies it) or stop depending on the fields that get trimmed.

What to grep for

  • Any pagination loop against /spaces/{spaceId}/roles that reads .total or increments .skip — those break on August 14. Migrate to reading pages.next and stopping when it's absent.
  • Any "done when items.length < limit" heuristic on a Contentful collection — replace with "done when there's no next cursor."
  • Any persisted numeric offset for roles pagination — it can't survive the cursor migration.
  • Any call to the space-users or space-members endpoints, plus the role of the token that makes it. Non-admin tokens reading user fields are the ones that silently lose data.

The tell for both changes is identical and easy to miss: a 200 OK that returns less than it did yesterday. There's no status code, no error body, and no changelog entry that fires inside your monitoring — the response is structurally valid and semantically wrong.


Schema drift like this — a field that stops populating, a pagination block that changes shape, a payload that thins out under a scoped token — is exactly the failure mode FlareCanary was built to catch: it watches your real API responses and tells you when their shape changes, before the silently-wrong data reaches your database.

Top comments (0)