From 3b97c1a1f9caf4b86ef3874b677d3597ac5a2826 Mon Sep 17 00:00:00 2001 From: Arya Kesarwani Date: Fri, 30 Oct 2026 02:51:19 +0530 Subject: [PATCH 1/2] fix(web): keep developer videos API paging from returning 500 --- .../unit/developer-list-params.test.ts | 23 +++++++++++++++++++ .../app/api/developer/v1/[...route]/videos.ts | 7 ++++-- apps/web/lib/developer-list-params.ts | 18 +++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 apps/web/__tests__/unit/developer-list-params.test.ts create mode 100644 apps/web/lib/developer-list-params.ts diff --git a/apps/web/__tests__/unit/developer-list-params.test.ts b/apps/web/__tests__/unit/developer-list-params.test.ts new file mode 100644 index 00000000000..812828c429f --- /dev/null +++ b/apps/web/__tests__/unit/developer-list-params.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { parseDeveloperListParams } from "@/lib/developer-list-params"; + +const parse = (limit?: string, offset?: string) => + parseDeveloperListParams(limit, offset); + +describe("parseDeveloperListParams", () => { + it("keeps the existing defaults and cap", () => { + expect(parse()).toEqual({ limit: 50, offset: 0 }); + expect(parse("0", "abc")).toEqual({ limit: 50, offset: 0 }); + expect(parse("25", "10")).toEqual({ limit: 25, offset: 10 }); + expect(parse("500")).toEqual({ limit: 100, offset: 0 }); + }); + + it("only passes whole numbers MySQL accepts in LIMIT and OFFSET", () => { + expect(parse("5.5", "2.5")).toEqual({ limit: 5, offset: 2 }); + expect(parse("-1", "-5")).toEqual({ limit: 1, offset: 0 }); + expect(parse("Infinity", "99999999999999999999")).toEqual({ + limit: 100, + offset: Number.MAX_SAFE_INTEGER, + }); + }); +}); diff --git a/apps/web/app/api/developer/v1/[...route]/videos.ts b/apps/web/app/api/developer/v1/[...route]/videos.ts index e8cbecdb1e4..72465d050d1 100644 --- a/apps/web/app/api/developer/v1/[...route]/videos.ts +++ b/apps/web/app/api/developer/v1/[...route]/videos.ts @@ -2,6 +2,7 @@ import { db } from "@cap/database"; import { developerVideos } from "@cap/database/schema"; import { and, desc, eq, isNull } from "drizzle-orm"; import { Hono } from "hono"; +import { parseDeveloperListParams } from "@/lib/developer-list-params"; import { withDeveloperSecretAuth } from "../../../utils"; export const app = new Hono<{ @@ -14,8 +15,10 @@ export const app = new Hono<{ app.get("/", async (c) => { const appId = c.get("developerAppId"); const userId = c.req.query("userId"); - const limit = Math.min(Number(c.req.query("limit") ?? 50) || 50, 100); - const offset = Math.max(0, Number(c.req.query("offset") ?? 0) || 0); + const { limit, offset } = parseDeveloperListParams( + c.req.query("limit"), + c.req.query("offset"), + ); const conditions = [ eq(developerVideos.appId, appId), diff --git a/apps/web/lib/developer-list-params.ts b/apps/web/lib/developer-list-params.ts new file mode 100644 index 00000000000..7f5ab8e09aa --- /dev/null +++ b/apps/web/lib/developer-list-params.ts @@ -0,0 +1,18 @@ +const DEFAULT_LIMIT = 50; +const MAX_LIMIT = 100; + +export function parseDeveloperListParams( + limit: string | undefined, + offset: string | undefined, +) { + return { + limit: Math.min( + Math.max(1, Math.trunc(Number(limit)) || DEFAULT_LIMIT), + MAX_LIMIT, + ), + offset: Math.min( + Math.max(0, Math.trunc(Number(offset)) || 0), + Number.MAX_SAFE_INTEGER, + ), + }; +} From 8a5cbdd100a526ebc89872bf4e92bd521aade98e Mon Sep 17 00:00:00 2001 From: Arya Kesarwani Date: Fri, 30 Oct 2026 03:57:49 +0530 Subject: [PATCH 2/2] fix(web): treat fractional developer API limits below one as one --- apps/web/__tests__/unit/developer-list-params.test.ts | 2 ++ apps/web/lib/developer-list-params.ts | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/web/__tests__/unit/developer-list-params.test.ts b/apps/web/__tests__/unit/developer-list-params.test.ts index 812828c429f..5ec87ff6cdb 100644 --- a/apps/web/__tests__/unit/developer-list-params.test.ts +++ b/apps/web/__tests__/unit/developer-list-params.test.ts @@ -15,6 +15,8 @@ describe("parseDeveloperListParams", () => { it("only passes whole numbers MySQL accepts in LIMIT and OFFSET", () => { expect(parse("5.5", "2.5")).toEqual({ limit: 5, offset: 2 }); expect(parse("-1", "-5")).toEqual({ limit: 1, offset: 0 }); + expect(parse("0.5")).toEqual({ limit: 1, offset: 0 }); + expect(parse("-0.5")).toEqual({ limit: 1, offset: 0 }); expect(parse("Infinity", "99999999999999999999")).toEqual({ limit: 100, offset: Number.MAX_SAFE_INTEGER, diff --git a/apps/web/lib/developer-list-params.ts b/apps/web/lib/developer-list-params.ts index 7f5ab8e09aa..c7d87659d8e 100644 --- a/apps/web/lib/developer-list-params.ts +++ b/apps/web/lib/developer-list-params.ts @@ -5,11 +5,11 @@ export function parseDeveloperListParams( limit: string | undefined, offset: string | undefined, ) { + const requestedLimit = Number(limit); return { - limit: Math.min( - Math.max(1, Math.trunc(Number(limit)) || DEFAULT_LIMIT), - MAX_LIMIT, - ), + limit: requestedLimit + ? Math.min(Math.max(1, Math.trunc(requestedLimit)), MAX_LIMIT) + : DEFAULT_LIMIT, offset: Math.min( Math.max(0, Math.trunc(Number(offset)) || 0), Number.MAX_SAFE_INTEGER,