From 42a6ec57e2e454db9b166ea0d786ce808c5501a1 Mon Sep 17 00:00:00 2001 From: luciano Date: Thu, 30 Apr 2026 00:07:11 +0200 Subject: [PATCH] =?UTF-8?q?fix(api):=20export=20PDF=20a=200=20byte=20?= =?UTF-8?q?=E2=80=94=20bufferizza=20chunks=20pdfkit=20prima=20di=20send?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Il pattern \`reply.send(doc)\` con uno stream pdfkit ha una race con fastify: il body può chiudersi prima che pdfkit emetta tutti i chunks dopo \`doc.end()\`. Ora colleziono i chunks in memoria, attendo \`doc.end()\` via Promise sull'evento \`end\`, e mando il Buffer completo con \`Content-Length\` corretto. Applicato a entrambi gli export (piano settimanale + tracking). Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/src/modules/plan/export.routes.ts | 18 +++++++++++++++--- apps/api/src/modules/tracking/export.routes.ts | 17 +++++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/apps/api/src/modules/plan/export.routes.ts b/apps/api/src/modules/plan/export.routes.ts index 02ed110..3d14057 100644 --- a/apps/api/src/modules/plan/export.routes.ts +++ b/apps/api/src/modules/plan/export.routes.ts @@ -52,9 +52,15 @@ export const planExportRoutes: FastifyPluginAsync = async (fastify) => { } const doc = new PDFDocument({ size: 'A4', margin: 50, bufferPages: true }); - reply.header('Content-Type', 'application/pdf'); - reply.header('Content-Disposition', 'attachment; filename="ketopath-piano.pdf"'); - reply.send(doc); + // Bufferizziamo i chunks: fastify + pdfkit stream hanno una race se + // facciamo `reply.send(doc)` e poi `doc.end()`. Con il buffer chiudiamo + // il PDF prima di inviarlo e siamo sicuri che il body sia completo. + const chunks: Buffer[] = []; + doc.on('data', (c: Buffer) => chunks.push(c)); + const done = new Promise((resolve, reject) => { + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + }); // ── Header ─────────────────────────────────────────────────────────── doc @@ -225,6 +231,12 @@ export const planExportRoutes: FastifyPluginAsync = async (fastify) => { } doc.end(); + const pdfBuffer = await done; + return reply + .header('Content-Type', 'application/pdf') + .header('Content-Disposition', 'attachment; filename="ketopath-piano.pdf"') + .header('Content-Length', String(pdfBuffer.length)) + .send(pdfBuffer); }, ); }; diff --git a/apps/api/src/modules/tracking/export.routes.ts b/apps/api/src/modules/tracking/export.routes.ts index 6f45371..950f8ae 100644 --- a/apps/api/src/modules/tracking/export.routes.ts +++ b/apps/api/src/modules/tracking/export.routes.ts @@ -39,10 +39,13 @@ export const trackingExportRoutes: FastifyPluginAsync = async (fastify) => { ]); const doc = new PDFDocument({ size: 'A4', margin: 50, bufferPages: true }); - - reply.header('Content-Type', 'application/pdf'); - reply.header('Content-Disposition', 'attachment; filename="ketopath-export.pdf"'); - reply.send(doc); + // Bufferizziamo i chunks per evitare race con fastify (vedi plan export). + const chunks: Buffer[] = []; + doc.on('data', (c: Buffer) => chunks.push(c)); + const done = new Promise((resolve, reject) => { + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', reject); + }); // ── Header ───────────────────────────────────────────────────────── doc @@ -151,5 +154,11 @@ export const trackingExportRoutes: FastifyPluginAsync = async (fastify) => { ); doc.end(); + const pdfBuffer = await done; + return reply + .header('Content-Type', 'application/pdf') + .header('Content-Disposition', 'attachment; filename="ketopath-export.pdf"') + .header('Content-Length', String(pdfBuffer.length)) + .send(pdfBuffer); }); };