Skip to content

Commit 1aa19cb

Browse files
Handle malformed affiliate offer JSON (#471)
1 parent c07c805 commit 1aa19cb

2 files changed

Lines changed: 35 additions & 3 deletions

File tree

src/app/api/affiliates/offers/route.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,23 @@ describe("POST /api/affiliates/offers", () => {
170170
vi.clearAllMocks();
171171
});
172172

173+
it("returns 400 for malformed JSON before touching affiliate offers", async () => {
174+
mockGetAuthContext.mockResolvedValue({ user: { id: "user1" } });
175+
176+
const req = new NextRequest("http://localhost/api/affiliates/offers", {
177+
method: "POST",
178+
body: "{not valid json",
179+
headers: { "Content-Type": "application/json" },
180+
});
181+
182+
const res = await POST(req);
183+
const body = await res.json();
184+
185+
expect(res.status).toBe(400);
186+
expect(body.error).toBe("Invalid JSON body");
187+
expect(mockFrom).not.toHaveBeenCalled();
188+
});
189+
173190
it("rejects javascript: URL in product_url (#18)", async () => {
174191
mockGetAuthContext.mockResolvedValue({ user: { id: "user1" } });
175192

src/app/api/affiliates/offers/route.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { escapePostgrestSearchValue } from "@/lib/security/sanitize";
66

77
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88
type AnySupabase = any;
9-
import { validateOfferInput } from "@/lib/affiliates/validation";
9+
import { validateOfferInput, type OfferInput } from "@/lib/affiliates/validation";
1010

1111
function parsePaginationParam(
1212
value: string | null,
@@ -28,6 +28,18 @@ function slugify(text: string): string {
2828
return slug || "offer";
2929
}
3030

31+
async function readJsonObject(request: NextRequest) {
32+
try {
33+
const body = await request.json();
34+
if (!body || typeof body !== "object" || Array.isArray(body)) {
35+
return null;
36+
}
37+
return body as Record<string, unknown>;
38+
} catch {
39+
return null;
40+
}
41+
}
42+
3143
/**
3244
* GET /api/affiliates/offers - List affiliate offers (public marketplace)
3345
*/
@@ -148,8 +160,11 @@ export async function POST(request: NextRequest) {
148160
const rl = checkRateLimit(getRateLimitIdentifier(request, auth.user.id), "write");
149161
if (!rl.allowed) return rateLimitExceeded(rl);
150162

151-
const body = await request.json();
152-
const validation = validateOfferInput(body);
163+
const body = await readJsonObject(request);
164+
if (!body) {
165+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
166+
}
167+
const validation = validateOfferInput(body as unknown as OfferInput);
153168

154169
if (!validation.ok) {
155170
return NextResponse.json({ error: validation.errors.join("; ") }, { status: 400 });

0 commit comments

Comments
 (0)