diff --git a/src/app/api/feed/route.test.ts b/src/app/api/feed/route.test.ts index ca49bda2..91216be2 100644 --- a/src/app/api/feed/route.test.ts +++ b/src/app/api/feed/route.test.ts @@ -112,6 +112,45 @@ describe("GET /api/feed", () => { expect(json.pagination.totalPages).toBe(5); }); + it("truncates fractional pagination params before applying range", async () => { + const chain = chainResult({ data: [], error: null, count: 50 }); + mockFrom.mockReturnValue(chain); + + const res = await GET(makeRequest({ page: "2.9", limit: "3.8" })); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(chain.range).toHaveBeenCalledWith(3, 5); + expect(json.pagination.page).toBe(2); + expect(json.pagination.limit).toBe(3); + }); + + it("falls back from non-finite pagination params to safe defaults", async () => { + const chain = chainResult({ data: [], error: null, count: 50 }); + mockFrom.mockReturnValue(chain); + + const res = await GET(makeRequest({ page: "Infinity", limit: "NaN" })); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(chain.range).toHaveBeenCalledWith(0, 19); + expect(json.pagination.page).toBe(1); + expect(json.pagination.limit).toBe(20); + }); + + it("falls back from mixed numeric strings instead of partially parsing them", async () => { + const chain = chainResult({ data: [], error: null, count: 50 }); + mockFrom.mockReturnValue(chain); + + const res = await GET(makeRequest({ page: "3abc", limit: "7px" })); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(chain.range).toHaveBeenCalledWith(0, 19); + expect(json.pagination.page).toBe(1); + expect(json.pagination.limit).toBe(20); + }); + // ── Sort modes ───────────────────────────────────────────────── it("sort=new orders by created_at descending", async () => { diff --git a/src/app/api/feed/route.ts b/src/app/api/feed/route.ts index 97eb3971..a3e8bd74 100644 --- a/src/app/api/feed/route.ts +++ b/src/app/api/feed/route.ts @@ -3,6 +3,19 @@ import { createClient } from "@/lib/supabase/server"; import { feedFiltersSchema } from "@/lib/validations"; import { getAuthContext } from "@/lib/auth/get-user"; +function parsePaginationParam( + value: string | null, + defaultValue: number, + min: number, + max: number +) { + const parsed = Number(value ?? defaultValue); + if (!Number.isFinite(parsed)) { + return defaultValue; + } + return Math.min(Math.max(Math.trunc(parsed), min), max); +} + // GET /api/feed - Paginated feed with sort options export async function GET(request: NextRequest) { try { @@ -11,8 +24,8 @@ export async function GET(request: NextRequest) { const filters = feedFiltersSchema.safeParse({ sort: searchParams.get("sort") || "hot", tag: searchParams.get("tag") || undefined, - page: Number(searchParams.get("page")) || 1, - limit: Number(searchParams.get("limit")) || 20, + page: parsePaginationParam(searchParams.get("page"), 1, 1, 10_000), + limit: parsePaginationParam(searchParams.get("limit"), 20, 1, 50), }); if (!filters.success) {