From 79bb77151b6f9313c209f4f272e1153f57b7c46b Mon Sep 17 00:00:00 2001 From: Alu-card19 Date: Thu, 25 Jun 2026 15:34:58 +0100 Subject: [PATCH 1/7] fix: sanitize non-numeric pagination query parameters Fixes issue #358 where non-numeric pagination parameters (page, limit, skip) caused 500 errors across list endpoints. Changes: - Add parsePaginationParam() helper in lib/validation.ts to safely parse and validate pagination parameters with configurable min/max bounds - Replaces unsafe parseInt() calls across all affected endpoints: * /api/posts * /api/posts/[id]/entries * /api/posts/[id]/comments * /api/users/[id]/followers * /api/users/[id]/following * /api/wallet/transactions * /api/leaderboard * /api/notifications * /api/discovery The helper function: - Returns default values when parsing fails (NaN result) - Validates minimum and maximum bounds - Prevents invalid values from reaching Prisma queries - Gracefully handles edge cases without throwing errors --- app/app/api/discovery/route.ts | 16 +++++--- app/app/api/leaderboard/route.ts | 17 +++++---- app/app/api/notifications/route.ts | 12 +++++- app/app/api/posts/[id]/comments/route.ts | 12 +++++- app/app/api/posts/[id]/entries/route.ts | 17 +++++---- app/app/api/posts/route.ts | 12 +++++- app/app/api/users/[id]/followers/route.ts | 12 +++++- app/app/api/users/[id]/following/route.ts | 12 +++++- app/app/api/wallet/transactions/route.ts | 15 +++++--- app/lib/validation.ts | 46 +++++++++++++++++++++++ 10 files changed, 136 insertions(+), 35 deletions(-) diff --git a/app/app/api/discovery/route.ts b/app/app/api/discovery/route.ts index 21ac7bc..99cc721 100644 --- a/app/app/api/discovery/route.ts +++ b/app/app/api/discovery/route.ts @@ -2,6 +2,7 @@ import { apiError, apiSuccess } from "@/lib/api-response"; import { prisma } from "@/lib/prisma"; import { NextRequest } from "next/server"; import { Prisma } from "@prisma/client"; +import { parsePaginationParam } from "@/lib/validation"; type DiscoveryResultType = "post" | "user" | "topic"; type DiscoveryRankBy = "relevance" | "recent" | "popular"; @@ -375,18 +376,21 @@ export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); const query = normalizeQuery(searchParams.get("q")); - const page = parseInt(searchParams.get("page") || "1", 10); - const limit = parseInt(searchParams.get("limit") || String(DEFAULT_LIMIT), 10); + const page = parsePaginationParam(searchParams.get("page"), { + defaultValue: 1, + min: 1, + }); + const limit = parsePaginationParam(searchParams.get("limit"), { + defaultValue: DEFAULT_LIMIT, + min: 1, + max: MAX_LIMIT, + }); const rankBy = (searchParams.get("rankBy") || "relevance") as DiscoveryRankBy; const period = searchParams.get("period") || "all-time"; const postType = searchParams.get("postType"); const postStatus = searchParams.get("status"); const requestedTypes = parseRequestedTypes(searchParams.get("types")); - if (page < 1 || limit < 1 || limit > MAX_LIMIT) { - return apiError("Invalid pagination parameters", 400); - } - if (!["relevance", "recent", "popular"].includes(rankBy)) { return apiError("Invalid ranking option", 400); } diff --git a/app/app/api/leaderboard/route.ts b/app/app/api/leaderboard/route.ts index 9e11c74..d1d76ee 100644 --- a/app/app/api/leaderboard/route.ts +++ b/app/app/api/leaderboard/route.ts @@ -2,18 +2,21 @@ import { apiError, apiSuccess } from '@/lib/api-response'; import { NextRequest } from 'next/server'; import { prisma } from '@/lib/prisma'; +import { parsePaginationParam } from '@/lib/validation'; export async function GET (request: NextRequest) { try { const { searchParams } = new URL(request.url); const period = searchParams.get('period') || 'all-time'; - const page = parseInt(searchParams.get('page') || '1'); - const limit = parseInt(searchParams.get('limit') || '50'); - - // Validate pagination parameters - if (page < 1 || limit < 1 || limit > 100) { - return apiError('Invalid pagination parameters', 400); - } + const page = parsePaginationParam(searchParams.get('page'), { + defaultValue: 1, + min: 1, + }); + const limit = parsePaginationParam(searchParams.get('limit'), { + defaultValue: 50, + min: 1, + max: 100, + }); let dateFilter: Date | undefined; if (period === 'weekly') { diff --git a/app/app/api/notifications/route.ts b/app/app/api/notifications/route.ts index 7c35781..258e4f1 100644 --- a/app/app/api/notifications/route.ts +++ b/app/app/api/notifications/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/auth"; +import { parsePaginationParam } from "@/lib/validation"; import { getPaginatedNotifications, markAllAsRead, @@ -16,8 +17,15 @@ export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); const isRead = searchParams.get("isRead"); - const page = Number.parseInt(searchParams.get("page") || "1", 10); - const pageSize = Number.parseInt(searchParams.get("pageSize") || "20", 10); + const page = parsePaginationParam(searchParams.get("page"), { + defaultValue: 1, + min: 1, + }); + const pageSize = parsePaginationParam(searchParams.get("pageSize"), { + defaultValue: 20, + min: 1, + max: 100, + }); const result = await getPaginatedNotifications({ userId: currentUser.id, diff --git a/app/app/api/posts/[id]/comments/route.ts b/app/app/api/posts/[id]/comments/route.ts index 514fff1..0dc76ba 100644 --- a/app/app/api/posts/[id]/comments/route.ts +++ b/app/app/api/posts/[id]/comments/route.ts @@ -4,6 +4,7 @@ import { apiSuccess, apiError } from "@/lib/api-response"; import { getCurrentUser } from "@/lib/auth"; import { readJsonBody } from "@/lib/parse-json-body"; import { createNotification } from "@/lib/notifications"; +import { parsePaginationParam } from "@/lib/validation"; export async function GET( request: NextRequest, @@ -12,8 +13,15 @@ export async function GET( try { const { id } = await params; const { searchParams } = new URL(request.url); - const page = parseInt(searchParams.get("page") || "1"); - const limit = parseInt(searchParams.get("limit") || "50"); + const page = parsePaginationParam(searchParams.get("page"), { + defaultValue: 1, + min: 1, + }); + const limit = parsePaginationParam(searchParams.get("limit"), { + defaultValue: 50, + min: 1, + max: 100, + }); const skip = (page - 1) * limit; const [comments, total] = await Promise.all([ diff --git a/app/app/api/posts/[id]/entries/route.ts b/app/app/api/posts/[id]/entries/route.ts index 0af1d07..2cd1a11 100644 --- a/app/app/api/posts/[id]/entries/route.ts +++ b/app/app/api/posts/[id]/entries/route.ts @@ -7,6 +7,7 @@ import { getCurrentUser } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { readJsonBody } from "@/lib/parse-json-body"; import { checkAndAwardBadges } from "@/lib/badges"; +import { parsePaginationParam } from "@/lib/validation"; import { createNotificationInTransaction, fanOutNotificationsInTransaction, @@ -303,15 +304,17 @@ export async function GET( const { id: postId } = await params; const { searchParams } = new URL(request.url); - const page = parseInt(searchParams.get("page") || "1"); - const limit = parseInt(searchParams.get("limit") || "10"); + const page = parsePaginationParam(searchParams.get("page"), { + defaultValue: 1, + min: 1, + }); + const limit = parsePaginationParam(searchParams.get("limit"), { + defaultValue: 10, + min: 1, + max: 100, + }); const skip = (page - 1) * limit; - // Validate pagination params - if (page < 1 || limit < 1 || limit > 100) { - return apiError("Invalid pagination parameters", 400); - } - // Check if post exists const post = await prisma.post.findUnique({ where: { id: postId }, diff --git a/app/app/api/posts/route.ts b/app/app/api/posts/route.ts index 5a69f04..18fb89d 100644 --- a/app/app/api/posts/route.ts +++ b/app/app/api/posts/route.ts @@ -9,6 +9,7 @@ import { prisma } from "@/lib/prisma"; import { readJsonBody } from "@/lib/parse-json-body"; import { POST_SLUG_MAX_LENGTH, sanitizePostSlug } from "@/lib/post-slug"; import { checkAndAwardBadges } from "@/lib/badges"; +import { parsePaginationParam } from "@/lib/validation"; const SLUG_SUFFIX_LENGTH = 6; @@ -215,8 +216,15 @@ const GET = async (request: NextRequest) => { const status = searchParams.get("status"); const from = searchParams.get("from"); const to = searchParams.get("to"); - const page = parseInt(searchParams.get("page") || "1"); - const limit = parseInt(searchParams.get("limit") || "10"); + const page = parsePaginationParam(searchParams.get("page"), { + defaultValue: 1, + min: 1, + }); + const limit = parsePaginationParam(searchParams.get("limit"), { + defaultValue: 10, + min: 1, + max: 100, + }); const where: Prisma.PostWhereInput = {}; where.moderationStatus = { notIn: ["suspended", "banned"] }; diff --git a/app/app/api/users/[id]/followers/route.ts b/app/app/api/users/[id]/followers/route.ts index 6ca4d0f..2db67a3 100644 --- a/app/app/api/users/[id]/followers/route.ts +++ b/app/app/api/users/[id]/followers/route.ts @@ -1,6 +1,7 @@ import { NextRequest } from 'next/server'; import { prisma } from '@/lib/prisma'; import { apiSuccess, apiError } from '@/lib/api-response'; +import { parsePaginationParam } from '@/lib/validation'; export async function GET( request: NextRequest, @@ -9,8 +10,15 @@ export async function GET( try { const { id: targetUserId } = await params; const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit') || '20'); - const skip = parseInt(searchParams.get('skip') || '0'); + const limit = parsePaginationParam(searchParams.get('limit'), { + defaultValue: 20, + min: 1, + max: 100, + }); + const skip = parsePaginationParam(searchParams.get('skip'), { + defaultValue: 0, + min: 0, + }); // Followers are users who follow the target user // i.e., followingId = targetUserId diff --git a/app/app/api/users/[id]/following/route.ts b/app/app/api/users/[id]/following/route.ts index 8e60fe2..430ca34 100644 --- a/app/app/api/users/[id]/following/route.ts +++ b/app/app/api/users/[id]/following/route.ts @@ -1,6 +1,7 @@ import { NextRequest } from 'next/server'; import { prisma } from '@/lib/prisma'; import { apiSuccess, apiError } from '@/lib/api-response'; +import { parsePaginationParam } from '@/lib/validation'; export async function GET( request: NextRequest, @@ -9,8 +10,15 @@ export async function GET( try { const { id: targetUserId } = await params; const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit') || '20'); - const skip = parseInt(searchParams.get('skip') || '0'); + const limit = parsePaginationParam(searchParams.get('limit'), { + defaultValue: 20, + min: 1, + max: 100, + }); + const skip = parsePaginationParam(searchParams.get('skip'), { + defaultValue: 0, + min: 0, + }); // Following are users the target user follows // i.e., userId = targetUserId, followingId != null diff --git a/app/app/api/wallet/transactions/route.ts b/app/app/api/wallet/transactions/route.ts index 535975b..a19fadc 100644 --- a/app/app/api/wallet/transactions/route.ts +++ b/app/app/api/wallet/transactions/route.ts @@ -3,6 +3,7 @@ import { apiError, apiSuccess } from "@/lib/api-response"; import { getCurrentUser } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { getLiveTransactions } from "@/lib/stellar"; +import { parsePaginationParam } from "@/lib/validation"; /** * GET /api/wallet/transactions @@ -20,11 +21,15 @@ export async function GET(request: NextRequest) { if (!currentUser) return apiError("Unauthorized", 401); const { searchParams } = new URL(request.url); - const page = Math.max(1, parseInt(searchParams.get("page") ?? "1", 10)); - const limit = Math.min( - 50, - Math.max(1, parseInt(searchParams.get("limit") ?? "20", 10)), - ); + const page = parsePaginationParam(searchParams.get("page"), { + defaultValue: 1, + min: 1, + }); + const limit = parsePaginationParam(searchParams.get("limit"), { + defaultValue: 20, + min: 1, + max: 50, + }); const skip = (page - 1) * limit; const [transactions, total, user] = await Promise.all([ diff --git a/app/lib/validation.ts b/app/lib/validation.ts index d5c1102..de41e3b 100644 --- a/app/lib/validation.ts +++ b/app/lib/validation.ts @@ -108,3 +108,49 @@ export const validateTargetAmount = (amount: number | string): ValidationResult } return { isValid: true }; }; + +/** + * Safely parse pagination parameters from query strings. + * Returns default values if parsing fails or values are invalid. + * @param value The query parameter value + * @param defaultValue The default value if parsing fails (default: based on param type) + * @param min Minimum allowed value (default: 0 for skip, 1 for page, 1 for limit) + * @param max Maximum allowed value (optional) + * @returns The parsed integer or default value + */ +export const parsePaginationParam = ( + value: string | null, + options?: { + defaultValue?: number; + min?: number; + max?: number; + } +): number => { + const defaultValue = options?.defaultValue ?? 1; + const min = options?.min ?? 1; + const max = options?.max; + + // If no value provided, return default + if (!value) { + return defaultValue; + } + + // Try to parse as integer + const parsed = parseInt(value, 10); + + // Check if parsing failed (result is NaN) + if (isNaN(parsed)) { + return defaultValue; + } + + // Check bounds + if (parsed < min) { + return defaultValue; + } + + if (max !== undefined && parsed > max) { + return max; + } + + return parsed; +}; From 1b589fbd97b9c0d8a4e4bb2a70560b6dfeb28ec7 Mon Sep 17 00:00:00 2001 From: Alu-card19 Date: Thu, 25 Jun 2026 15:54:50 +0100 Subject: [PATCH 2/7] test: update pagination tests for graceful parameter handling Updated tests to reflect new pagination behavior: - Invalid/non-numeric parameters now use sensible defaults instead of 400 error - Added test for handling non-numeric pagination parameters - Tests now verify default values are used for invalid input - Maintains validation for other query parameters (e.g., rankBy, period) --- app/tests/api/discovery.test.ts | 15 ++++++++++++++- app/tests/api/entries.test.ts | 29 ++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/app/tests/api/discovery.test.ts b/app/tests/api/discovery.test.ts index e62764c..4913c30 100644 --- a/app/tests/api/discovery.test.ts +++ b/app/tests/api/discovery.test.ts @@ -234,7 +234,20 @@ describe("Discovery API", () => { const response = await GET(request); const { status, data } = await parseResponse(response); + // rankBy=unknown should return 400, but pagination params now default gracefully expect(status).toBe(400); expect(data.success).toBe(false); }); -}); + + it("handles invalid pagination parameters gracefully", async () => { + const request = createMockRequest( + "http://localhost:3000/api/discovery?q=test&page=abc&limit=xyz", + ); + const response = await GET(request); + const { status, data } = await parseResponse(response); + + expect(status).toBe(200); + expect(data.success).toBe(true); + expect(data.data.pagination.page).toBe(1); + expect(data.data.pagination.limit).toBe(10); + }); diff --git a/app/tests/api/entries.test.ts b/app/tests/api/entries.test.ts index 3426a2d..62c7887 100644 --- a/app/tests/api/entries.test.ts +++ b/app/tests/api/entries.test.ts @@ -723,9 +723,14 @@ describe('Entry API Endpoints', () => { }); }); - it('should reject invalid pagination parameters', async () => { + it('should handle invalid pagination parameters gracefully by using defaults', async () => { + const mockEntries = []; + prisma.post.findUnique = vi.fn().mockResolvedValue(post); + prisma.entry.findMany = vi.fn().mockResolvedValue(mockEntries); + prisma.entry.count = vi.fn().mockResolvedValue(0); + const request = createMockRequest( - `http://localhost:3000/api/posts/${post.id}/entries?page=0&limit=-1`, + `http://localhost:3000/api/posts/${post.id}/entries?page=abc&limit=xyz`, ); const response = await GetEntries(request, { @@ -733,12 +738,18 @@ describe('Entry API Endpoints', () => { }); const { status, data } = await parseResponse(response); - expect(status).toBe(400); - expect(data.success).toBe(false); - expect(data.error).toBe('Invalid pagination parameters'); + expect(status).toBe(200); + expect(data.success).toBe(true); + expect(data.data.pagination.page).toBe(1); + expect(data.data.pagination.limit).toBe(10); }); - it('should reject limit greater than 100', async () => { + it('should cap limit at 100', async () => { + const mockEntries = []; + prisma.post.findUnique = vi.fn().mockResolvedValue(post); + prisma.entry.findMany = vi.fn().mockResolvedValue(mockEntries); + prisma.entry.count = vi.fn().mockResolvedValue(0); + const request = createMockRequest( `http://localhost:3000/api/posts/${post.id}/entries?limit=101`, ); @@ -748,9 +759,9 @@ describe('Entry API Endpoints', () => { }); const { status, data } = await parseResponse(response); - expect(status).toBe(400); - expect(data.success).toBe(false); - expect(data.error).toBe('Invalid pagination parameters'); + expect(status).toBe(200); + expect(data.success).toBe(true); + expect(data.data.pagination.limit).toBe(100); }); it('should return 404 for non-existent post', async () => { From a9fefcfb6da8b9a52d8b5ac95a91fe08069bcb70 Mon Sep 17 00:00:00 2001 From: Alu-card19 Date: Thu, 25 Jun 2026 15:58:37 +0100 Subject: [PATCH 3/7] fix: close describe block in discovery.test.ts Fixed parse error in tests/api/discovery.test.ts where the describe block was missing its closing brace. This was causing vitest to fail with: [PARSE_ERROR] Expected } but found EOF --- app/tests/api/discovery.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/tests/api/discovery.test.ts b/app/tests/api/discovery.test.ts index 4913c30..6f1d3e5 100644 --- a/app/tests/api/discovery.test.ts +++ b/app/tests/api/discovery.test.ts @@ -251,3 +251,5 @@ describe("Discovery API", () => { expect(data.data.pagination.page).toBe(1); expect(data.data.pagination.limit).toBe(10); }); + +}); From 3bc422171d541480e005889ccf71b48b45af98fd Mon Sep 17 00:00:00 2001 From: Alu-card19 Date: Thu, 25 Jun 2026 17:21:58 +0100 Subject: [PATCH 4/7] fix: use Number() instead of parseInt() for more reliable NaN handling in pagination params --- app/lib/validation.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/lib/validation.ts b/app/lib/validation.ts index de41e3b..e2cb308 100644 --- a/app/lib/validation.ts +++ b/app/lib/validation.ts @@ -135,14 +135,17 @@ export const parsePaginationParam = ( return defaultValue; } - // Try to parse as integer - const parsed = parseInt(value, 10); + // Try to parse as number + const rawParsed = Number(value); // Check if parsing failed (result is NaN) - if (isNaN(parsed)) { + if (Number.isNaN(rawParsed)) { return defaultValue; } + // Floor to integer + const parsed = Math.floor(rawParsed); + // Check bounds if (parsed < min) { return defaultValue; From 4a47bbebfb4b9d52bb6110e7afdb2e0fd3ea6258 Mon Sep 17 00:00:00 2001 From: Alu-card19 Date: Thu, 25 Jun 2026 17:30:50 +0100 Subject: [PATCH 5/7] fix: add explicit validation and error handling for pagination parameters in discovery route --- app/app/api/discovery/route.ts | 52 ++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/app/app/api/discovery/route.ts b/app/app/api/discovery/route.ts index 99cc721..3d91aad 100644 --- a/app/app/api/discovery/route.ts +++ b/app/app/api/discovery/route.ts @@ -375,16 +375,49 @@ async function searchTopics(args: { export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); - const query = normalizeQuery(searchParams.get("q")); - const page = parsePaginationParam(searchParams.get("page"), { + + // Parse and validate pagination parameters + const rawPage = searchParams.get("page"); + const rawLimit = searchParams.get("limit"); + + const page = parsePaginationParam(rawPage, { defaultValue: 1, min: 1, }); - const limit = parsePaginationParam(searchParams.get("limit"), { + const limit = parsePaginationParam(rawLimit, { defaultValue: DEFAULT_LIMIT, min: 1, max: MAX_LIMIT, }); + + // Validate that page and limit are valid numbers + if (!Number.isFinite(page) || page < 1 || !Number.isFinite(limit) || limit < 1) { + console.error("[discovery] Invalid pagination after parsing", { + rawPage, + rawLimit, + page, + limit, + }); + // Return gracefully with defaults instead of 500 + return apiSuccess({ + query: null, + results: [], + pagination: { + page: 1, + limit: DEFAULT_LIMIT, + total: 0, + totalPages: 0, + hasMore: false, + }, + ranking: { + rankBy: "relevance", + period: "all-time", + }, + counts: { post: 0, user: 0, topic: 0 }, + }); + } + + const query = normalizeQuery(searchParams.get("q")); const rankBy = (searchParams.get("rankBy") || "relevance") as DiscoveryRankBy; const period = searchParams.get("period") || "all-time"; const postType = searchParams.get("postType"); @@ -437,7 +470,14 @@ export async function GET(request: NextRequest) { const resultSets = await Promise.all(tasks); const combinedResults = rankResults(resultSets.flat(), rankBy); const total = combinedResults.length; - const paginatedResults = combinedResults.slice((page - 1) * limit, page * limit); + + // Validate calculations before using them + const skip = (page - 1) * limit; + const paginatedResults = combinedResults.slice(skip, skip + limit); + + // Ensure limit is never 0 to avoid division by zero + const safeLimit = Math.max(limit, 1); + const totalPages = Math.ceil(total / safeLimit); const counts = combinedResults.reduce( (acc, result) => { @@ -454,7 +494,7 @@ export async function GET(request: NextRequest) { page, limit, total, - totalPages: Math.ceil(total / limit), + totalPages, hasMore: page * limit < total, }, ranking: { @@ -464,7 +504,7 @@ export async function GET(request: NextRequest) { counts, }); } catch (error) { - console.error("Discovery API error:", error); + console.error("[discovery] route error:", error); return apiError("Failed to fetch discovery results", 500); } } From 23e75060ad0c74460d01043be4e63eba6fb5b54f Mon Sep 17 00:00:00 2001 From: Alu-card19 Date: Thu, 25 Jun 2026 17:35:43 +0100 Subject: [PATCH 6/7] fix: remove duplicate #[allow(clippy::too_many_arguments)] attribute in giveaway.rs --- contracts/geev-core/src/giveaway.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/geev-core/src/giveaway.rs b/contracts/geev-core/src/giveaway.rs index 75eb136..40d1eea 100644 --- a/contracts/geev-core/src/giveaway.rs +++ b/contracts/geev-core/src/giveaway.rs @@ -29,7 +29,6 @@ pub struct GiveawayWinnerSelected { prize_amount: i128, } -#[allow(clippy::too_many_arguments)] #[contractimpl] impl GiveawayContract { #[allow(clippy::too_many_arguments)] From 111a3e7d0d312ac5394ae3a667ab6726a63db65c Mon Sep 17 00:00:00 2001 From: Alu-card19 Date: Thu, 25 Jun 2026 17:38:06 +0100 Subject: [PATCH 7/7] fix: replace parsePaginationParam with inline Number-based parsing in discovery route --- app/app/api/discovery/route.ts | 53 +++++----------------------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/app/app/api/discovery/route.ts b/app/app/api/discovery/route.ts index 3d91aad..b1c409f 100644 --- a/app/app/api/discovery/route.ts +++ b/app/app/api/discovery/route.ts @@ -2,7 +2,6 @@ import { apiError, apiSuccess } from "@/lib/api-response"; import { prisma } from "@/lib/prisma"; import { NextRequest } from "next/server"; import { Prisma } from "@prisma/client"; -import { parsePaginationParam } from "@/lib/validation"; type DiscoveryResultType = "post" | "user" | "topic"; type DiscoveryRankBy = "relevance" | "recent" | "popular"; @@ -376,46 +375,11 @@ export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); - // Parse and validate pagination parameters - const rawPage = searchParams.get("page"); - const rawLimit = searchParams.get("limit"); - - const page = parsePaginationParam(rawPage, { - defaultValue: 1, - min: 1, - }); - const limit = parsePaginationParam(rawLimit, { - defaultValue: DEFAULT_LIMIT, - min: 1, - max: MAX_LIMIT, - }); - - // Validate that page and limit are valid numbers - if (!Number.isFinite(page) || page < 1 || !Number.isFinite(limit) || limit < 1) { - console.error("[discovery] Invalid pagination after parsing", { - rawPage, - rawLimit, - page, - limit, - }); - // Return gracefully with defaults instead of 500 - return apiSuccess({ - query: null, - results: [], - pagination: { - page: 1, - limit: DEFAULT_LIMIT, - total: 0, - totalPages: 0, - hasMore: false, - }, - ranking: { - rankBy: "relevance", - period: "all-time", - }, - counts: { post: 0, user: 0, topic: 0 }, - }); - } + // Parse and validate pagination parameters directly + const rawPage = Number(searchParams.get("page")); + const rawLimit = Number(searchParams.get("limit")); + const page = Number.isNaN(rawPage) || rawPage < 1 ? 1 : Math.floor(rawPage); + const limit = Number.isNaN(rawLimit) || rawLimit < 1 ? 20 : Math.min(Math.floor(rawLimit), 100); const query = normalizeQuery(searchParams.get("q")); const rankBy = (searchParams.get("rankBy") || "relevance") as DiscoveryRankBy; @@ -471,13 +435,10 @@ export async function GET(request: NextRequest) { const combinedResults = rankResults(resultSets.flat(), rankBy); const total = combinedResults.length; - // Validate calculations before using them + // Use sanitized page and limit values const skip = (page - 1) * limit; const paginatedResults = combinedResults.slice(skip, skip + limit); - - // Ensure limit is never 0 to avoid division by zero - const safeLimit = Math.max(limit, 1); - const totalPages = Math.ceil(total / safeLimit); + const totalPages = Math.ceil(total / limit); const counts = combinedResults.reduce( (acc, result) => {