mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 03:55:38 -04:00
Harden API input validation, error handling, and optimistic rollbacks
Add try/catch around `request.json()` in all POST routes to return a 400 instead of crashing on malformed bodies. Validate `type` as a strict `"movie" | "tv"` enum in search, discover, import, and resolve routes. Validate `tmdbId` as a positive integer. Add a `SORT_BY_PATTERN` regex and page-range check (1–500) to the discover route. Wrap all outbound TMDB calls in try/catch and return 502 on failure so clients get a structured error rather than an unhandled rejection. Fix optimistic-update rollbacks in `use-title-actions`: capture `prevStatus` and `prevWatches` before each mutation and restore both atoms in the catch block for catchUp, handleMarkSeason, handleUnmarkSeason, and single-episode toggle. Fix a bug in `getContinueWatchingFeed` where the watchDateMap could hold a stale date for episodes watched more than once; the map now keeps the most-recent `watchedAt` per episode.
This commit is contained in:
@@ -12,16 +12,38 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { tmdbId, type } = body;
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!tmdbId || !type || !["movie", "tv"].includes(type)) {
|
||||
const parsed = body as { tmdbId?: unknown; type?: unknown };
|
||||
const type = parsed.type;
|
||||
const tmdbId =
|
||||
typeof parsed.tmdbId === "number"
|
||||
? parsed.tmdbId
|
||||
: Number.parseInt(String(parsed.tmdbId), 10);
|
||||
|
||||
if (
|
||||
!Number.isInteger(tmdbId) ||
|
||||
tmdbId < 1 ||
|
||||
(type !== "movie" && type !== "tv")
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "tmdbId and type (movie|tv) are required" },
|
||||
{ error: "tmdbId (positive integer) and type (movie|tv) are required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const title = await importTitle(tmdbId, type);
|
||||
return NextResponse.json(title);
|
||||
try {
|
||||
const title = await importTitle(tmdbId, type);
|
||||
return NextResponse.json(title);
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to import title" },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,38 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { tmdbId, type } = body;
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await req.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!tmdbId || !type || !["movie", "tv"].includes(type)) {
|
||||
const parsed = body as { tmdbId?: unknown; type?: unknown };
|
||||
const type = parsed.type;
|
||||
const tmdbId =
|
||||
typeof parsed.tmdbId === "number"
|
||||
? parsed.tmdbId
|
||||
: Number.parseInt(String(parsed.tmdbId), 10);
|
||||
|
||||
if (
|
||||
!Number.isInteger(tmdbId) ||
|
||||
tmdbId < 1 ||
|
||||
(type !== "movie" && type !== "tv")
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "tmdbId and type (movie|tv) are required" },
|
||||
{ error: "tmdbId (positive integer) and type (movie|tv) are required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const title = await importTitle(tmdbId, type, { awaitEnrichment: true });
|
||||
return NextResponse.json({ id: title?.id });
|
||||
try {
|
||||
const title = await importTitle(tmdbId, type, { awaitEnrichment: true });
|
||||
return NextResponse.json({ id: title?.id });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to resolve title" },
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user