Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/app/api/users/[username]/followers/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,24 @@ describe("GET /api/users/[username]/followers", () => {

expect(follows.range).toHaveBeenCalledWith(30, 39);
});

it("caps huge offsets before applying range", async () => {
const targetProfile = profileChain({ data: { id: "user-123" }, error: null });
const follows = followsChain({ data: [], error: null, count: 0 });

let callCount = 0;
mockFrom.mockImplementation(() => {
callCount++;
return callCount === 1 ? targetProfile : follows;
});

const res = await GET(
makeRequest({ limit: "10", offset: "999999999" }),
routeParams
);
const json = await res.json();

expect(follows.range).toHaveBeenCalledWith(100000, 100009);
expect(json.pagination.offset).toBe(100000);
});
});
8 changes: 5 additions & 3 deletions src/app/api/users/[username]/followers/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";

const MAX_FOLLOW_OFFSET = 100_000;

function parsePositiveInt(value: string | null, fallback: number, max: number): number {
const parsed = Number.parseInt(value ?? "", 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
Expand All @@ -9,12 +11,12 @@ function parsePositiveInt(value: string | null, fallback: number, max: number):
return Math.min(parsed, max);
}

function parseNonNegativeInt(value: string | null, fallback: number): number {
function parseNonNegativeInt(value: string | null, fallback: number, max: number): number {
const parsed = Number.parseInt(value ?? "", 10);
if (!Number.isFinite(parsed) || parsed < 0) {
return fallback;
}
return parsed;
return Math.min(parsed, max);
}
Comment on lines 4 to 20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicated constant and utility functions across route files

MAX_FOLLOW_OFFSET, parsePositiveInt, and parseNonNegativeInt are defined identically in both followers/route.ts and following/route.ts. If the cap value or parsing logic needs to change (e.g., lowering the max, fixing an edge case), both files must be updated in sync — a future maintainer will likely only update one. Extracting these to a shared module (e.g., src/app/api/users/[username]/pagination.ts) would eliminate the drift risk.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


// GET /api/users/[username]/followers - list a user's followers
Expand All @@ -27,7 +29,7 @@ export async function GET(
const supabase = await createClient();
const searchParams = request.nextUrl.searchParams;
const limit = parsePositiveInt(searchParams.get("limit"), 20, 100);
const offset = parseNonNegativeInt(searchParams.get("offset"), 0);
const offset = parseNonNegativeInt(searchParams.get("offset"), 0, MAX_FOLLOW_OFFSET);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silently capped offset reflected back in pagination metadata

When a client sends offset=999999999, the response returns pagination.offset: 100000 with no indication that the value was clamped. A cursor-driven client that uses the returned offset to drive its next page request (e.g., assumes returned_offset + limit is the next page start) will silently resume from 100000 rather than seeing an error. Consider returning a 400 for offsets above the cap, or at minimum including a field like pagination.offset_clamped: true so callers can detect the truncation.


// Look up target user
const { data: targetProfile, error: profileError } = await supabase
Expand Down
20 changes: 20 additions & 0 deletions src/app/api/users/[username]/following/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,24 @@ describe("GET /api/users/[username]/following", () => {

expect(follows.range).toHaveBeenCalledWith(30, 39);
});

it("caps huge offsets before applying range", async () => {
const targetProfile = profileChain({ data: { id: "user-123" }, error: null });
const follows = followsChain({ data: [], error: null, count: 0 });

let callCount = 0;
mockFrom.mockImplementation(() => {
callCount++;
return callCount === 1 ? targetProfile : follows;
});

const res = await GET(
makeRequest({ limit: "10", offset: "999999999" }),
routeParams
);
const json = await res.json();

expect(follows.range).toHaveBeenCalledWith(100000, 100009);
expect(json.pagination.offset).toBe(100000);
});
});
8 changes: 5 additions & 3 deletions src/app/api/users/[username]/following/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";

const MAX_FOLLOW_OFFSET = 100_000;

function parsePositiveInt(value: string | null, fallback: number, max: number): number {
const parsed = Number.parseInt(value ?? "", 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
Expand All @@ -9,12 +11,12 @@ function parsePositiveInt(value: string | null, fallback: number, max: number):
return Math.min(parsed, max);
}

function parseNonNegativeInt(value: string | null, fallback: number): number {
function parseNonNegativeInt(value: string | null, fallback: number, max: number): number {
const parsed = Number.parseInt(value ?? "", 10);
if (!Number.isFinite(parsed) || parsed < 0) {
return fallback;
}
return parsed;
return Math.min(parsed, max);
}

// GET /api/users/[username]/following - list who a user follows
Expand All @@ -27,7 +29,7 @@ export async function GET(
const supabase = await createClient();
const searchParams = request.nextUrl.searchParams;
const limit = parsePositiveInt(searchParams.get("limit"), 20, 100);
const offset = parseNonNegativeInt(searchParams.get("offset"), 0);
const offset = parseNonNegativeInt(searchParams.get("offset"), 0, MAX_FOLLOW_OFFSET);

// Look up target user
const { data: targetProfile, error: profileError } = await supabase
Expand Down
Loading