diff --git a/src/app/api/measurements/series-batch/__tests__/route.test.ts b/src/app/api/measurements/series-batch/__tests__/route.test.ts index dc6e31c02..9e465acfb 100644 --- a/src/app/api/measurements/series-batch/__tests__/route.test.ts +++ b/src/app/api/measurements/series-batch/__tests__/route.test.ts @@ -117,4 +117,24 @@ describe("GET /api/measurements/series-batch", () => { expect(res.status).toBe(200); expect(vi.mocked(readDailySeries)).toHaveBeenCalledTimes(1); }); + + it("isolates a single type's failure to an empty slice, others still return", async () => { + vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); + const weightRows = [ + { type: "WEIGHT", value: 80, measuredAt: FROM, count: 1 }, + ]; + vi.mocked(readDailySeries).mockImplementation(async ({ type }) => { + if (type === "WEIGHT") return weightRows as never; + throw new Error("transient db reject"); + }); + + const res = await GET( + req(`types=WEIGHT,BLOOD_PRESSURE_SYS&from=${FROM}&to=${TO}`), + ); + // One type rejecting must NOT 500 the whole batch. + expect(res.status).toBe(200); + const body = await res.json(); + expect(body.data.series.WEIGHT).toEqual(weightRows); + expect(body.data.series.BLOOD_PRESSURE_SYS).toEqual([]); + }); }); diff --git a/src/app/api/measurements/series-batch/route.ts b/src/app/api/measurements/series-batch/route.ts index 8f685926c..00f1ace89 100644 --- a/src/app/api/measurements/series-batch/route.ts +++ b/src/app/api/measurements/series-batch/route.ts @@ -63,18 +63,27 @@ export const GET = apiHandler(async (request: NextRequest) => { // so the batched reader doesn't re-query it per type. const priorityJson = await loadUserSourcePriority(user.id); + // Per-type isolation: a single type's transient DB reject degrades to an + // empty slice for THAT type only, never a 500 that blanks every chart in + // the row. The failing chart self-fetches through its existing fallback. const limit = pLimit(SERIES_BATCH_CONCURRENCY); + let degradedTypes = 0; const entries = await Promise.all( uniqueTypes.map((type) => - limit(async () => { - const rows = await readDailySeries({ - userId: user.id, - type, - from, - to, - priorityJson, - }); - return [type, rows] as const; + limit(async (): Promise => { + try { + const rows = await readDailySeries({ + userId: user.id, + type, + from, + to, + priorityJson, + }); + return [type, rows] as const; + } catch { + degradedTypes += 1; + return [type, []] as const; + } }), ), ); @@ -89,6 +98,7 @@ export const GET = apiHandler(async (request: NextRequest) => { meta: { type_count: uniqueTypes.length, total: entries.reduce((sum, [, rows]) => sum + rows.length, 0), + degraded_types: degradedTypes, }, });