node.jspdfnodemailernode-streamsnode-pdfkit

Node PDFKit pipe to multiple targets


I'm having a problem, when I have to pipe() the created document to multiple targets, in my case to a HTTP response and to an email attachment using node-mailer. After the first use in an attachment to an email, nothing gets piped to a response (when calling it from the client, the PDF has 0 bytes).

Response controller:

const doc = await createPdf(course.name, lastRecordDate, ctx);   

// Send a notificiation email with attachment
if (query.hasOwnProperty('sendEmail') && query.sendEmail === 'true') {
  await sendNotificationEmail(doc, course, ctx);   
}

doc.pipe(res);
res.contentType('application/pdf');

Function to send an email:

async function sendNotificationEmail(doc: any, course: Course, ctx: Context) {
  const attachment = {
    filename: `${course.name}-certificate.pdf`,
    contentType: 'application/pdf',
    content: doc
  };
  return SMTPSendTemplateWithAttachments(
    ctx,
    ['somememail@test.si'],
    `${course.name}`,
    'en-report-created',
    {
      firstName: ctx.user.firstName,
      courseName: course.name
    },
    [attachment]
  );
}

If I remove the function to send an email, the PDF gets normally piped to a response and I can download it from the client.

I tried to find a way to clone the stream (PDFKit's document is a stream as far as I know), but was unsuccessful.

Any solution would be really helpful.


Solution

  • I solved the problem using two PassThrough streams two which I piped the PDFKitdocument Stream, then on the data event I wrote the chunks to two separate buffers. On the end event I send the data with an email and created a new PassThrough stream and piped it to the response.

    Here's the code.

    // Both PassThrough streams are defined before in order to use them in the createPdf function
    streamCopy1 = new PassThrough();
    streamCopy2 = new PassThrough();
    
    const buffer1 = [];
    const buffer2 = [];
    
    streamCopy1
      .on('data', (chunk) => {
        buffer1.push(chunk);
      })
      .on('end', () => {
        const bufferFinished = Buffer.concat(buffer1);
        if (query.hasOwnProperty('sendEmail') && query.sendEmail === 'true') {
          sendNotificationEmail(bufferFinished, course, ctx);
        }
      });
    
    streamCopy2
      .on('data', (chunk) => {
        buffer2.push(chunk);
      })
      .on('end', () => {
        const bufferFinished = Buffer.concat(buffer2);
        const stream = new PassThrough();
        stream.push(bufferFinished);
        stream.end();
        stream.pipe(res);
        res.contentType('application/pdf');
      });
    

    The function to create the PDF.

    // function declaration
    const doc = new pdfDocument();
    doc.pipe(streamCopy1).pipe(streamCopy2);
    // rest of the code
    

    The solution is not really the nicest, any suggestions are very welcome.