What
GET /channels (server/src/channels/routes.ts:1066-1073) and GET /api/admin/people (server/src/app.ts:587-599) parse paging with:
const limit = Number.parseInt(url.searchParams.get("limit") ?? "", 10);
...(Number.isFinite(limit) ? { limit } : {}),
parseInt coerces, so ?limit=12abc → 12, ?limit=3.9 → 3, ?limit=-5 → -5 (then clamped to 1 in the store), ?limit=0 → 1, ?limit=999999 → MAX. All 200 with a silently coerced page. There is no 400 path at all.
The audit list route (server/src/audit.ts:647-655 auditQueryFromUrl) already does the strict thing: trim + /^\d+\$/ + clamp 1..100 (falling back to 50 only when absent).
Repro
GET /channels?limit=12abc → 200 with limit 12 instead of 400.
GET /channels?limit=3.9 → 200 with limit 3.
GET /api/admin/people?limit=-5 → 200 with limit 1.
- Huge
?limit=999999 forces a MAX+1 DB read every time instead of being clamped at the edge.
Expected: malformed limit → 400 naming the parameter; valid integers clamped 1..MAX at the edge like the store already does; absent stays default. Same behavior on both endpoints.
Where it runs
Stateless query parsing in the server process. Same 400/clamp on every replica; the store clamp stays as the second line of defence.
Fix sketch
Shared strict limit parser mirroring auditQueryFromUrl (trim, /^\d+\$/, 400 on mismatch, clamp 1..MAX), used by both routes. Add route tests for 12abc/3.9/-5/0/absent/huge.
What
GET /channels(server/src/channels/routes.ts:1066-1073) andGET /api/admin/people(server/src/app.ts:587-599) parse paging with:parseIntcoerces, so?limit=12abc→ 12,?limit=3.9→ 3,?limit=-5→ -5 (then clamped to 1 in the store),?limit=0→ 1,?limit=999999→ MAX. All 200 with a silently coerced page. There is no 400 path at all.The audit list route (
server/src/audit.ts:647-655 auditQueryFromUrl) already does the strict thing: trim +/^\d+\$/+ clamp 1..100 (falling back to 50 only when absent).Repro
GET /channels?limit=12abc→ 200 with limit 12 instead of 400.GET /channels?limit=3.9→ 200 with limit 3.GET /api/admin/people?limit=-5→ 200 with limit 1.?limit=999999forces a MAX+1 DB read every time instead of being clamped at the edge.Expected: malformed limit → 400 naming the parameter; valid integers clamped 1..MAX at the edge like the store already does; absent stays default. Same behavior on both endpoints.
Where it runs
Stateless query parsing in the server process. Same 400/clamp on every replica; the store clamp stays as the second line of defence.
Fix sketch
Shared strict limit parser mirroring
auditQueryFromUrl(trim,/^\d+\$/, 400 on mismatch, clamp 1..MAX), used by both routes. Add route tests for 12abc/3.9/-5/0/absent/huge.