Skip to content

Commit 90e4e4d

Browse files
committed
fix(rest): the export door truncates its page chunk where it reads it, so no fractional limit reaches the normalizer
Claude-Session: https://claude.ai/code/session_01A3FoRXqj2EgAzPQbXU3Kzh Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8845dd2 commit 90e4e4d

2 files changed

Lines changed: 22 additions & 2 deletions

File tree

‎packages/rest/src/data-action-routes.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,7 +1020,13 @@ export function registerDataActionEndpoints(this: DataActionRouteHost, basePath:
10201020
// the same rows in the same order, so no value of it can widen or
10211021
// substitute the answer, and refusing one would turn a correct
10221022
// export into a `400`.
1023-
const chunkSize = Math.min(MAX_CHUNK, Math.max(50, q.page != null ? Number(q.page) || 500 : 500));
1023+
//
1024+
// [#25229] …which is why it is TRUNCATED where it is read: the
1025+
// chunk becomes each `findData`'s `limit`, and the normalizer
1026+
// refuses a fractional window (`?page=100.5` was a `take` of
1027+
// 100.5, answered 400 naming `limit`, a parameter the caller
1028+
// never wrote).
1029+
const chunkSize = Math.min(MAX_CHUNK, Math.max(50, q.page != null ? Math.trunc(Number(q.page)) || 500 : 500));
10241030
// Colour cells only for xlsx within the style cap; decided up
10251031
// front (before streaming) since we can't know the true row
10261032
// count until the stream drains.

‎packages/rest/src/rest-server-query-number-census.test.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,13 +457,27 @@ describe('#20139 census — exemption: export `?page=` changes chunking, never t
457457
const absent = await exportWith({});
458458
expect(absent.status).toBe(200);
459459
expect(JSON.parse(absent.streamed)).toHaveLength(ROWS.length);
460-
for (const page of ['abc', '', ' ', '1.5', 'Infinity', '-5', '50', '5000']) {
460+
for (const page of ['abc', '', ' ', '1.5', '100.5', 'Infinity', '-5', '50', '5000']) {
461461
const answer = await exportWith({ page });
462462
expect(answer.status, `?page=${JSON.stringify(page)}`).toBe(200);
463463
expect(answer.streamed, `?page=${JSON.stringify(page)} changed the exported rows`).toBe(absent.streamed);
464464
}
465465
});
466466

467+
it('[#25229] every chunk handed to `findData` is a whole count, whatever `page` holds', async () => {
468+
// The normalizer refuses a fractional or negative window with a 400
469+
// naming `limit` — a parameter an export caller never wrote. `100.5`
470+
// survives the 50..5000 clamp, so only truncation at the read keeps it
471+
// out; this stub `findData` records the window it was handed.
472+
for (const page of ['abc', '', '1.5', '100.5', 'Infinity', '-5', '50', '5000']) {
473+
const { takes } = await exportWith({ page });
474+
expect(takes.length, `?page=${JSON.stringify(page)}`).toBeGreaterThan(0);
475+
for (const take of takes) {
476+
expect(Number.isSafeInteger(take) && take >= 0, `?page=${JSON.stringify(page)} handed findData limit ${take}`).toBe(true);
477+
}
478+
}
479+
});
480+
467481
it('an unreadable `page` is answered exactly as an absent one: the default 500-row chunk', async () => {
468482
const absent = await exportWith({});
469483
expect(absent.takes).toEqual([500, 500, 200]);

0 commit comments

Comments
 (0)