From 09094cfc3034869aee2887c961e70df511defe3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Quang=20H=C3=B2a?= Date: Tue, 26 May 2026 14:51:21 +0700 Subject: [PATCH] fix(benchmarks): stop leaking 'latest' placeholder to postgres date cast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /api/v1/benchmarks GET was throwing a 500 with 'Invalid time value' on initial page load. Root cause: useBenchmarks(model, undefined) → benchmarkQueryOptions(model, date ?? 'latest') // fallback marker → fetchBenchmarks(model, 'latest', …) // passes 'latest'! → GET /api/v1/benchmarks?date=latest → getLatestBenchmarks(sql, dbModelKeys, 'latest') → SQL: AND br.date <= ${'latest'}::date → postgres-js: new Date('latest') → NaN → Date.prototype.toISOString → RangeError('Invalid time value') → 500 The 'latest' string was only intended as a queryKey marker for "no specific date", but it leaked through to the fetch URL. Fix: - benchmarkQueryOptions: skip the date when it's the 'latest' sentinel, keep it in the queryKey for cache discrimination. dateForFetch is undefined → no `date` param in the URL → API uses the no-date latest_benchmarks MV path. - /api/v1/benchmarks GET: defensive ISO-date regex validation. Anything that isn't `YYYY-MM-DD` is treated as no date. Prevents future leaks from hitting the same 500. Worked in upstream's data shape because the upstream defaults race-loaded quickly, but on our small DB the empty-date initial render persisted long enough to show the 500 to users. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/app/src/app/api/v1/benchmarks/route.ts | 8 +++++++- packages/app/src/hooks/api/use-benchmarks.ts | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/app/src/app/api/v1/benchmarks/route.ts b/packages/app/src/app/api/v1/benchmarks/route.ts index c79f1aa74..9e9641820 100644 --- a/packages/app/src/app/api/v1/benchmarks/route.ts +++ b/packages/app/src/app/api/v1/benchmarks/route.ts @@ -20,10 +20,16 @@ const getCachedBenchmarks = cachedQuery( { blobOnly: true }, ); +const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/u; + export async function GET(request: NextRequest) { const params = request.nextUrl.searchParams; const model = params.get('model') ?? ''; - const date = params.get('date') ?? undefined; + // Reject anything that isn't an ISO YYYY-MM-DD — otherwise postgres-js will + // coerce `${date}::date` via `new Date(date)`, which on garbage produces NaN + // and throws `Invalid time value` from toISOString → opaque 500. + const rawDate = params.get('date'); + const date = rawDate && ISO_DATE_RE.test(rawDate) ? rawDate : undefined; const exact = params.get('exact') === 'true'; const dbModelKeys = DISPLAY_MODEL_TO_DB[model]; if (!dbModelKeys || dbModelKeys.length === 0) { diff --git a/packages/app/src/hooks/api/use-benchmarks.ts b/packages/app/src/hooks/api/use-benchmarks.ts index 6da1568e3..2087c27db 100644 --- a/packages/app/src/hooks/api/use-benchmarks.ts +++ b/packages/app/src/hooks/api/use-benchmarks.ts @@ -9,9 +9,15 @@ export function benchmarkQueryOptions( enabled = true, exact?: boolean, ) { + // 'latest' is only a queryKey marker meaning "no specific date" — must NOT + // be sent to the API as a date filter (postgres-js coerces `${date}::date` + // params via `new Date(date)`, which on 'latest' produces NaN and throws + // `Invalid time value` from toISOString → 500 to the client). + const dateForFetch = date === 'latest' ? undefined : date; return { queryKey: ['benchmarks', model, date, exact ? 'exact' : 'latest'] as const, - queryFn: ({ signal }: { signal: AbortSignal }) => fetchBenchmarks(model, date, exact, signal), + queryFn: ({ signal }: { signal: AbortSignal }) => + fetchBenchmarks(model, dateForFetch, exact, signal), enabled: enabled && Boolean(model), }; }