From e22c9226d8f06c27e9e6450790429dc186bc0e68 Mon Sep 17 00:00:00 2001 From: Joel Brock Date: Wed, 10 Jun 2026 11:34:54 -0700 Subject: [PATCH] Staff report: query Attachment.get per file id (no IN clause) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The {IN: [...]} operator object for `id` crashes Civi APIv3 on this install — Civi's error renderer calls htmlentities() on the array value and dies, producing the 500 with the html error page seen at 6c3e8dd's deploy. Single-id calls succeed (confirmed in the user's API Explorer test against id=150). So loop one call per file id and run them in parallel via Promise.allSettled. Reports typically reference a handful of files, so the round-trip overhead is small and individual file failures don't poison the whole report. --- app/api/staff/report/route.ts | 50 +++++++++++++++++------------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/app/api/staff/report/route.ts b/app/api/staff/report/route.ts index c13e3ce..05ecb0b 100644 --- a/app/api/staff/report/route.ts +++ b/app/api/staff/report/route.ts @@ -374,35 +374,33 @@ async function buildLivePayload(orgId: number): Promise { const urlByFileId = new Map(); if (fileIds.size > 0) { - try { - const attachRes = await civi3<{ id: string | number; url?: string }>( - "Attachment", - "get", - { - // APIv3 takes id as an IN-clause via the {IN: [...]} operator object. - id: { IN: Array.from(fileIds) }, - // APIv3 expects `return` as a comma-separated string. Passing an - // array crashes Civi's error pathway on htmlentities() — that's - // v4 syntax. + // APIv3 Attachment.get doesn't accept an IN-clause cleanly on this Civi + // install — passing {IN: [...]} for `id` crashes Civi's error renderer + // on htmlentities(array). The same call with `id: ` works + // (verified in API Explorer), so we loop one call per file id. Reports + // typically reference a handful of files, so the round-trip cost is + // small. Each request is independent; we issue them in parallel. + const lookups = await Promise.allSettled( + Array.from(fileIds).map((fid) => + civi3<{ id: string | number; url?: string }>("Attachment", "get", { + id: fid, return: "id,url", - // sequential: 1 forces values to be returned as a plain array - // rather than an object keyed by id. The civi3 helper normalizes - // either, but the array form is the canonical APIv3 client shape. sequential: 1, - options: { limit: 0 }, - }, - ); - for (const a of attachRes.values ?? []) { - const fid = Number(a.id); - if (Number.isFinite(fid) && typeof a.url === "string" && a.url.length > 0) { - urlByFileId.set(fid, a.url); - } + }).then((r) => ({ fid, row: r.values?.[0] })), + ), + ); + for (const result of lookups) { + if (result.status === "rejected") { + console.warn( + "[staff/report] Attachment.get (v3) failed for one file:", + result.reason instanceof Error ? result.reason.message : String(result.reason), + ); + continue; + } + const { fid, row } = result.value; + if (row && typeof row.url === "string" && row.url.length > 0) { + urlByFileId.set(fid, row.url); } - } catch (e) { - console.warn( - "[staff/report] Attachment.get (v3) failed; file links will lack fcs:", - e instanceof Error ? e.message : String(e), - ); } }