Reputation: 381
I have a function that creates a pdf-file and sends it to email using pdfkit
and nodemailer
Every now or then I get a file I can't open. Can't figure out why this happens and why it works most of the time? I haven't noticed any certain situation when it fails, there doesn't seem to be any formula in it (text length etc). Could someone point out if there is some obvious problem in my pdf -creation code (like with async/await).
exports.sendTranscriptionToEmail = async (req, res) => {
let finalText = [];
let nickColorsArray = [];
const doc = new PDFDocument();
let filename = req.body.sessionName;
let text = [];
if (!filename || typeof filename != "string") {
return res.status(400).send({ message: "Incorrect Information!" });
}
// here I get the data from the database
try {
const rows = await knex("transcriptions").select("*").where({
conversation_id: filename,
});
if (!rows) {
return res.status(400).send({ message: "Transcription not found" });
}
// Stripping special characters
filename = encodeURIComponent(filename) + ".pdf";
res.setHeader(
"Content-disposition",
'attachment; filename="' + filename + '"'
);
res.setHeader("Content-type", "application/pdf");
doc.pipe(fs.createWriteStream(filename));
doc.fontSize(18).fillColor("black").text("Participants:", {
width: 410,
align: "center",
});
doc.moveDown();
nickColorsArray.forEach((n) => {
doc.fontSize(14).fillColor(n.color).text(n.nick, {
width: 410,
align: "center",
});
});
doc.moveDown();
doc.moveDown();
doc.fontSize(18).fillColor("black").text("Transcription:", {
width: 410,
align: "center",
});
doc.moveDown();
finalText.forEach((f) => {
doc
.fontSize(14)
.fillColor(f.color)
.text(f.word + " ", {
width: 410,
continued: true,
});
});
doc.end();
} catch (err) {
console.log("Something went wrong: ", err.message);
}
Upvotes: 2
Views: 1998
Reputation: 340
I have experienced the same issue, after investigation i found out my solution at https://github.com/foliojs/pdfkit/issues/265
PDFKit doesn't actually know when all of the data has been flushed to whatever stream you're writing to (file, http response, etc.). Since PDFKit has no access to the actual writable stream it is being piped to (PDFKit itself is a readable stream, and you set up the writable part), it only knows when it has finished pumping out chunks to whoever might be reading. It may be some time later that the writable stream actually flushes its internal buffers out to the actual destination.
I believe it's not quite as simple as listening for the 'finish' event on the write stream, specifically in the case of errors, so I implemented the following function which returns a Promise.
function savePdfToFile(pdf : PDFKit.PDFDocument, fileName : string) : Promise<void> {
return new Promise<void>((resolve, reject) => {
// To determine when the PDF has finished being written successfully
// we need to confirm the following 2 conditions:
//
// 1. The write stream has been closed
// 2. PDFDocument.end() was called syncronously without an error being thrown
let pendingStepCount = 2;
const stepFinished = () => {
if (--pendingStepCount == 0) {
resolve();
}
};
const writeStream = fs.createWriteStream(fileName);
writeStream.on('close', stepFinished);
pdf.pipe(writeStream);
pdf.end();
stepFinished();
});
}
Instead of calling .end() directly, you call this function and pass the pdf document and filename.
This should correctly handle the following situations:
PDF Generated successfully Error is thrown inside pdf.end() before write stream is closed Error is thrown inside pdf.end() after write stream has been closed
So in the end it seems that sometimes server does not create the file fast enough for you, after implementing this solution,my response time did increase by something like 1sec and got rid of this corruption behavior
Upvotes: 3