Skip to content

Refuse a malformed list limit instead of silently coercing it - #500

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
Ayush7614:fix/list-limit-strict-validation
Sep 12, 2026
Merged

Refuse a malformed list limit instead of silently coercing it#500
davidmckayv merged 2 commits into
CopilotKit:mainfrom
Ayush7614:fix/list-limit-strict-validation

Conversation

@Ayush7614

Copy link
Copy Markdown
Contributor

What this changes

Fixes #496. One shared parser for every paged list, factoring out the audit list's rule:

  • GET /channels and GET /api/admin/people read ?limit= with Number.parseInt, which coerces: ?limit=12abc arrived as 12, ?limit=3.9 as 3, ?limit=0x10 as 0, and each answered 200 with a silently wrong page. There was no 400 path at all.
  • New server/src/paging.ts parsePageLimit (same trim + /^\\d+\$/ rule as auditQueryFromUrl): absent/blank leaves the store default alone, a run of digits is clamped into 1..max against the same ceiling the store enforces (MAX_CHANNEL_PAGE / people MAX_PAGE, now exported so edge and store cannot drift apart), and anything else is a 400 naming the parameter before the database is reached. The stores' own clamps stay as the second line of defence.

Where it runs

Stateless query parsing in the server process. Same 400/clamp on every replica.

  • New state that outlives a request? None. Per-request parsing.
  • What happens on the second replica? Same 400/clamped page on every server: pure function of the query string. No coordination.
  • Anything serialised? No.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Postgres is already there and is the default answer to all of the above: nothing here needs it.

Boundary and audit

  • Every acting call still goes through the gateway: resolve, decide, audit, then act. Read-only list endpoints; auth and pagination semantics untouched.
  • New refusals and new failures each write a row. N/A: 400s here are caller errors answered at the edge before any store read.
  • Nothing new is trusted from the client that the server can resolve itself. Stricter: fewer shapes reach the database.

Changelog

  • A line in CHANGELOG.md under Unreleased: list limit validation entry.

Proof

  • bun test server/tests/paging.test.ts: 14 pass (absent/blank, well-formed, clamped 0/huge, 7 coerced shapes refused).
  • bun test server/tests/channel-routes.test.ts new channel list limit block: 9 pass (5 coerced refused with 400 + store never reached, 3 well-formed incl. clamp, absent default). File total 58 pass vs 49 on clean tree; the 29 remaining failures are pre-existing DB integration tests needing postgres (identical 29 fail on clean upstream main without my change).
  • bun test server/tests/paging.test.ts server/tests/audit.test.ts server/tests/audit-cursor.test.ts: 25 pass, 0 fail.
  • bunx biome format + bunx biome lint --error-on-warnings on touched files: clean.
  • bunx tsc --noEmit -p server/tsconfig.json: only the 3 pre-existing copilot.ts missing-module errors, identical on clean upstream main; nothing in touched files.

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep-reviewed clean (validation, no secret leak, fail-closed, agrees with existing layers). CI green.

@davidmckayv
davidmckayv merged commit f72abe7 into CopilotKit:main Sep 12, 2026
15 checks passed
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.

GET /channels and GET /api/admin/people coerce malformed limit instead of refusing it

2 participants