Skip to content

Commit 587c350

Browse files
authored
fix(search): clamp pagination bounds (#325)
1 parent 8b7e086 commit 587c350

2 files changed

Lines changed: 42 additions & 2 deletions

File tree

src/app/api/search/route.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,35 @@ describe("GET /api/search", () => {
272272
expect(chain.range).toHaveBeenCalledWith(0, 9);
273273
});
274274

275+
it("truncates fractional page values before calculating ranges", async () => {
276+
const chain = chainResult({ data: [], error: null, count: 0 });
277+
mockFrom.mockReturnValue(chain);
278+
279+
const res = await GET(
280+
makeRequest({ q: "test", type: "gigs", page: "2.9", limit: "5" })
281+
);
282+
const json = await res.json();
283+
284+
expect(res.status).toBe(200);
285+
expect(chain.range).toHaveBeenCalledWith(5, 9);
286+
expect(json.results.gigs.page).toBe(2);
287+
});
288+
289+
it("caps huge page values before calculating ranges", async () => {
290+
const chain = chainResult({ data: [], error: null, count: 0 });
291+
mockFrom.mockReturnValue(chain);
292+
293+
const res = await GET(
294+
makeRequest({ q: "test", type: "gigs", page: "1e308", limit: "999" })
295+
);
296+
const json = await res.json();
297+
298+
expect(res.status).toBe(200);
299+
expect(chain.range).toHaveBeenCalledWith(4999950, 4999999);
300+
expect(json.results.gigs.page).toBe(100000);
301+
expect(json.results.gigs.limit).toBe(50);
302+
});
303+
275304
// ── SQL character escaping ────────────────────────────────────
276305

277306
it("escapes % in search query", async () => {

src/app/api/search/route.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,25 @@ import { createClient } from "@/lib/supabase/server";
33

44
type SearchType = "gigs" | "agents" | "posts" | "all";
55

6+
function parsePaginationParam(
7+
value: string | null,
8+
defaultValue: number,
9+
min: number,
10+
max: number
11+
) {
12+
const parsed = Number(value && value.trim() !== "" ? value : defaultValue);
13+
const finiteValue = Number.isFinite(parsed) ? parsed : defaultValue;
14+
return Math.min(Math.max(Math.trunc(finiteValue), min), max);
15+
}
16+
617
// GET /api/search?q=<query>&type=gigs|agents|posts|all&page=1&limit=10
718
export async function GET(request: NextRequest) {
819
try {
920
const searchParams = request.nextUrl.searchParams;
1021
const query = searchParams.get("q")?.trim() || "";
1122
const type = (searchParams.get("type") || "all") as SearchType;
12-
const page = Math.max(1, Number(searchParams.get("page")) || 1);
13-
const limit = Math.min(50, Math.max(1, Number(searchParams.get("limit")) || 10));
23+
const page = parsePaginationParam(searchParams.get("page"), 1, 1, 100_000);
24+
const limit = parsePaginationParam(searchParams.get("limit"), 10, 1, 50);
1425

1526
if (!query) {
1627
return NextResponse.json(

0 commit comments

Comments
 (0)