fix(api): export PDF a 0 byte — bufferizza chunks pdfkit prima di send

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) <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.7 committed 2026-04-30 00:07:11 +02:00
1 parent e015abff33
commit 42a6ec57e2
2 files changed
+28 -7

No files matched your search

+15 -3
View File
@@ -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<Buffer>((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);
},
);
};
+13 -4
View File
@@ -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<Buffer>((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);
});
};