1
votes

I'm using NestJS 8.0.2 and I'm trying to return a PDF from an API endpoint using the new StreamableFile class which, as the documentation says:

A StreamableFile is a class that holds onto the stream that is to be returned. To create a new StreamableFile, you can pass either a Buffer or a Stream to the StreamableFile constructor.

I'm using a PDF printing library called pdfMake, which provides an option to return the PDF from memory as a Buffer object. I'm trying to avoid saving the generated PDF to the filesystem if possible and just return it directly.

So I'm trying to combine the two by doing the following:

pdf-service.ts

async generatePDF(inputs: PDFInputsDTO) {
  try {
    const definition = this.generateDocDefinition(inputs);
    const document = pdfMake.createPdf(definition);
    const promise = new Promise((resolve, reject) => {
      try {
        document.getBuffer((result) => {
          resolve(result); // result is of type Buffer
      } catch (e) {
        reject(e);
      }
    });

    return promise as Promise<Buffer>;
  } catch (e) {
    throw new InternalServiceException('PDF generation error');
  }
}

pdf-controller.ts

@Post('pdf')
@HttpCode(200)
@UseFilters(ValidationExceptionFilter)
@UsePipes(new ValidationPipe(PdfController.pipeOptions))
@Header('Content-Type', 'application/pdf')
@Header('Content-Disposition', 'inline; filename=file.pdf')
async generatePDFFile(@Body() inputs: PDFInputsDTO) {
  try {
    const pdfFile = await this.pdfService.generatePDF(inputs);

    console.log(`PDF is: ${pdfFile}`); // prints "PDF is: <Buffer 25 50 44 46 2d 31 2e 33 0a 25 ff ff ff ff 0a 39 20 30 20 6f 62 6a 0a 3c 3c 0a 2f 54 79 70 65 20 2f 45 78 74 47 53 74 61 74 65 0a 2f 63 61 20 31 0a 2f ... >"

    return new StreamableFile(pdfFile);
  } catch(e) {
    throw(e);
  }
}

This results in an exception:

[Nest] 26539  - 07/24/2021, 4:14:33 PM   ERROR [ExceptionsHandler] Cannot read property 'pipe' of undefined
TypeError: Cannot read property 'pipe' of undefined
    at ExpressAdapter.reply (/Users/agentlogic/Work/Web/Backend/pdf-generator/node_modules/@nestjs/platform-express/adapters/express-adapter.js:27:36)
    at RouterResponseController.apply (/Users/agentlogic/Work/Web/Backend/pdf-generator/node_modules/@nestjs/core/router/router-response-controller.js:14:36)
    at /Users/agentlogic/Work/Web/Backend/pdf-generator/node_modules/@nestjs/core/router/router-execution-context.js:175:48
    at processTicksAndRejections (internal/process/task_queues.js:93:5)
    at /Users/agentlogic/Work/Web/Backend/pdf-generator/node_modules/@nestjs/core/router/router-execution-context.js:47:13
    at /Users/agentlogic/Work/Web/Backend/pdf-generator/node_modules/@nestjs/core/router/router-proxy.js:9:17

ADDITIONAL INFO

Using the Response class from express works, but I want to be able to respond as JSON if there are any input validation errors (using UseFilters and UsePipes):

async generatePDFFile(@Body() inputs: PDFInputsDTO, @Res() response: Response) {
  try {
    const pdfFile = await this.pdfService.generatePDF(inputs);

    response.set({ 'Content-Length': pdfFile.length });
    response.end(pdfFile);
  } catch (e)
    throw e;
  }
}

The above works, but I lose the ability to automatically respond with any input validation errors as as JSON object.

do console.log(Buffer.isBuffer(pdfFile)) prints true?Micael Levi
I think that if you use return printer.createPdfKitDocument(definition) (as shown here) in your generatePDF, it will work.Micael Levi
@MicaelLevi Oh, console.log(Buffer.isBuffer(pdfFile)) prints false... Your helpful comment led me to do an additional const buffer = Buffer.from(pdfFile) and return that, and it now responds with Content-Type: application/octet-stream instead of application/pdf...Agent.Logic_
Re: the github link, I'm not sure how to import pdfPrinter from the build file. The example imports it from the src directory...Agent.Logic_
PdfPrinter is imported from pdfmake. I just tested it. Its latest published versionMicael Levi