Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to return PDF file from controller

I try to return a PDF file from a Controller Endpoint using NestJs. When not setting the Content-type header, the data returned by getDocumentFile gets returned to the user just fine. When I add the header however, the return I get seems to be some strange form of a GUID, the response always looks like this: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx where x is a lowercase hexadecimal character. It also seems to be totally unrelated to the actual return value of the handler function, as I even get this strange GUID-thing when not returning anything at all.

When not setting Content-type: application/pdf, the function returns the data of the buffer just fine, however I need to set the header in order to get the browser to recognize the response as a PDF file which is important for my use case.

The controller looks like this:

@Controller('documents')
export class DocumentsController {
  constructor(private documentsService: DocumentsService) {}

  @Get(':id/file')
  @Header('Content-type', 'application/pdf')
  async getDocumentFile(@Param('id') id: string): Promise<Buffer> {
    const document = await this.documentsService.byId(id)
    const pdf = await this.documentsService.getFile(document)

    // using ReadableStreamBuffer as suggested by contributor
    const stream = new ReadableStreamBuffer({
      frequency: 10,
      chunkSize: 2048,
    })
    stream.put(pdf)
    return stream
  }
}

and my DocumentsService like this:

@Injectable()
export class DocumentsService {
  async getAll(): Promise<Array<DocumentDocument>> {
    return DocumentModel.find({})
  }

  async byId(id: string): Promise<DocumentDocument> {
    return DocumentModel.findOne({ _id: id })
  }

  async getFile(document: DocumentDocument): Promise<Buffer> {
    const filename = document.filename
    const filepath = path.join(__dirname, '..', '..', '..', '..', '..', 'pdf-generator', 'dist', filename)

    const pdf = await new Promise<Buffer>((resolve, reject) => {
      fs.readFile(filepath, {}, (err, data) => {
        if (err) reject(err)
        else resolve(data)
      })
    })
    return pdf
  }
}

I originally just returned the buffer (return pdf), but that brought the same result as the attempt above. On the repository of NestJs a user suggested to use the above method, which obviously does not work for me either. See the GitHub thread here.

like image 895
Tom Doodler Avatar asked Nov 27 '18 17:11

Tom Doodler


3 Answers

Updated in 2021 too:

I prefer this way as I don't need the post-controller interceptor logic. We can control the name of the file and make it be inline or downloading the file.

@Get()
  download(@Res() res) {
    const filename = '123.pdf';
    // make it to be inline other than downloading
    // res.setHeader('Content-disposition', 'inline; filename=' + filename);
    res.setHeader('Content-disposition', 'attachment; filename=' + filename);
    const filestream = createReadStream('files/' + filename);
    filestream.pipe(res);
  }

More info in the offical Nest Docs: https://docs.nestjs.com/techniques/streaming-files

like image 82
passport4j Avatar answered Sep 18 '22 15:09

passport4j


Update 2021:

From now on in Nest Version 8 you can use the class StreamableFile:

import { Controller, Get, StreamableFile } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';

@Controller('file')
export class FileController {
  @Get()
  getFile(): StreamableFile {
    const file = createReadStream(join(process.cwd(), 'package.json'));
    return new StreamableFile(file);
  }
}

More info in the offical Nest Docs: https://docs.nestjs.com/techniques/streaming-files

like image 12
Marc Bollmann Avatar answered Oct 21 '22 08:10

Marc Bollmann


You can just use ready decorator @Res this is my working solution:

Controller(NestJs):

async getNewsPdfById(@Param() getNewsParams: GetNewsPdfParams, @Req() request: Request, @Res() response: Response): Promise<void> {
  const stream = await this.newsService.getNewsPdfById(getNewsParams.newsId, request.user.ownerId);

  response.set({
    'Content-Type': 'image/pdf',
  });

  stream.pipe(response);
}

In my case stream variable is just ready stream created by html-pdf library because i create pdf by html https://www.npmjs.com/package/html-pdf but it doesnt matter how you create your stream. The thing is that you should use @Res decorator and pipe it because its native NestJs solution.

Also here is code how to claim file on client side: https://gist.github.com/javilobo8/097c30a233786be52070986d8cdb1743

Anyway lets try this one in your case:

@Controller('documents')
export class DocumentsController {
  constructor(private documentsService: DocumentsService) {}

  @Get(':id/file')
  async getDocumentFile(@Param('id') id: string, @Res res: Response): Promise<Buffer> {
    const document = await this.documentsService.byId(id)
    const pdf = await this.documentsService.getFile(document)


    const stream = new ReadableStreamBuffer({
      frequency: 10,
      chunkSize: 2048,
    })

    res.set({
      'Content-Type': 'image/pdf',
    });

    stream.pipe(res);
  }
}
like image 9
user3765649 Avatar answered Oct 21 '22 09:10

user3765649