From 55f68d2ca98322d4d00dfd72906887bec750cc84 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 24 Jul 2026 10:38:50 +0100 Subject: [PATCH] perf(webapp): clamp list-endpoint page size to 100 Several list endpoints accepted an unbounded page size. Clamp perPage / per_page / pageSize to 100 on the schedules, queues, versions, engine-report, and llm-models endpoints, matching the existing cap on the runs and sessions endpoints. Oversized requests now return up to 100 items and keep paginating instead of pulling an unbounded page. --- .server-changes/cap-list-endpoint-page-size.md | 6 ++++++ ...min.api.v1.environments.$environmentId.engine.report.ts | 7 ++++++- apps/webapp/app/routes/admin.api.v1.llm-models.ts | 5 ++++- apps/webapp/app/routes/api.v1.queues.ts | 7 ++++++- apps/webapp/app/routes/api.v1.schedules.ts | 7 ++++++- ...tionSlug.projects.$projectParam.env.$envParam.queues.ts | 6 +++++- ...onSlug.projects.$projectParam.env.$envParam.versions.ts | 6 +++++- 7 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 .server-changes/cap-list-endpoint-page-size.md diff --git a/.server-changes/cap-list-endpoint-page-size.md b/.server-changes/cap-list-endpoint-page-size.md new file mode 100644 index 00000000000..2649f305787 --- /dev/null +++ b/.server-changes/cap-list-endpoint-page-size.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +List API endpoints now clamp the page size to a maximum of 100. Requests asking for a larger page size return up to 100 items and keep paginating, rather than pulling an unbounded page. diff --git a/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.report.ts b/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.report.ts index 7179680d39a..4fdca9475fa 100644 --- a/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.report.ts +++ b/apps/webapp/app/routes/admin.api.v1.environments.$environmentId.engine.report.ts @@ -13,7 +13,12 @@ const ParamsSchema = z.object({ const SearchParamsSchema = z.object({ verbose: z.string().default("0"), page: z.coerce.number().optional(), - per_page: z.coerce.number().optional(), + per_page: z.coerce + .number() + .int() + .positive() + .transform((n) => Math.min(n, 100)) + .optional(), }); export async function loader({ request, params }: LoaderFunctionArgs) { diff --git a/apps/webapp/app/routes/admin.api.v1.llm-models.ts b/apps/webapp/app/routes/admin.api.v1.llm-models.ts index 1d8136ca37e..c35a1d65669 100644 --- a/apps/webapp/app/routes/admin.api.v1.llm-models.ts +++ b/apps/webapp/app/routes/admin.api.v1.llm-models.ts @@ -9,7 +9,10 @@ export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const page = parseInt(url.searchParams.get("page") ?? "1"); - const pageSize = parseInt(url.searchParams.get("pageSize") ?? "50"); + const pageSize = Math.max( + 1, + Math.min(parseInt(url.searchParams.get("pageSize") ?? "50") || 50, 100) + ); const [models, total] = await Promise.all([ prisma.llmModel.findMany({ diff --git a/apps/webapp/app/routes/api.v1.queues.ts b/apps/webapp/app/routes/api.v1.queues.ts index 0e976ec8134..b257a248a0b 100644 --- a/apps/webapp/app/routes/api.v1.queues.ts +++ b/apps/webapp/app/routes/api.v1.queues.ts @@ -12,7 +12,12 @@ import { ServiceValidationError } from "~/v3/services/baseService.server"; const SearchParamsSchema = z.object({ page: z.coerce.number().int().positive().optional(), - perPage: z.coerce.number().int().positive().optional(), + perPage: z.coerce + .number() + .int() + .positive() + .transform((n) => Math.min(n, 100)) + .optional(), }); export const loader = createLoaderApiRoute( diff --git a/apps/webapp/app/routes/api.v1.schedules.ts b/apps/webapp/app/routes/api.v1.schedules.ts index 510cf45eacf..b5fd2fd968c 100644 --- a/apps/webapp/app/routes/api.v1.schedules.ts +++ b/apps/webapp/app/routes/api.v1.schedules.ts @@ -12,7 +12,12 @@ import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.serv const SearchParamsSchema = z.object({ page: z.coerce.number().int().positive().optional(), - perPage: z.coerce.number().int().positive().optional(), + perPage: z.coerce + .number() + .int() + .positive() + .transform((n) => Math.min(n, 100)) + .optional(), }); export async function action({ request }: ActionFunctionArgs) { diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.ts index 98e31daa4d3..b5c1755eae4 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues.ts @@ -9,7 +9,11 @@ import { EnvironmentParamSchema } from "~/utils/pathBuilder"; const SearchParamsSchema = z.object({ query: z.string().optional(), page: z.coerce.number().min(1).default(1), - per_page: z.coerce.number().min(1).default(20), + per_page: z.coerce + .number() + .min(1) + .transform((n) => Math.min(n, 100)) + .default(20), type: z.enum(["task", "custom"]).optional(), }); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts index f17f2a95c8a..2275ab73bac 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts @@ -8,7 +8,11 @@ import { EnvironmentParamSchema } from "~/utils/pathBuilder"; const SearchParamsSchema = z.object({ query: z.string().optional(), - per_page: z.coerce.number().min(1).default(25), + per_page: z.coerce + .number() + .min(1) + .transform((n) => Math.min(n, 100)) + .default(25), }); export async function loader({ request, params }: LoaderFunctionArgs) {