【问题标题】:How to return PDF file from controller如何从控制器返回 PDF 文件
【发布时间】:2019-04-29 11:39:35
【问题描述】:

我尝试使用 NestJs 从控制器端点返回 PDF 文件。当不设置Content-type 标头时,getDocumentFile 返回的数据会很好地返回给用户。然而,当我添加标题时,我得到的返回似乎是某种奇怪形式的 GUID,响应总是如下所示:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 其中x 是一个小写的十六进制字符。它似乎也与处理函数的实际返回值完全无关,因为我什至在根本不返回任何东西时都会得到这个奇怪的 GUID。

当不设置Content-type: application/pdf 时,函数返回缓冲区的数据就好了,但是我需要设置标题以使浏览器将响应识别为PDF 文件,这对我的用例很重要。

控制器如下所示:

@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
  }
}

我的 DocumentsService 是这样的:

@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
  }
}

我最初只是返回了缓冲区 (return pdf),但这带来了与上述尝试相同的结果。在 NestJs 的存储库中,一位用户建议使用上述方法,这显然对我也不起作用。请参阅 GitHub 线程 here

【问题讨论】:

  • 没有错误,但是,如上所述,我没有得到 PDF 数据作为返回,而是一个看似随机的 GUID(顺便说一句,每个请求都不同)。没有任何错误消息,显然不是我想要的结果
  • 你找到解决办法了吗?
  • 很遗憾没有。你有同样的问题吗?
  • 不相似,但我无法使用 axios 从 React App 下载文件。我得到的只是空的 blob 数据或此输出 &lt;Buffer 25 50 44 46 ... &gt;

标签: javascript node.js nestjs


【解决方案1】:

它对我有用。

@Get('pdf')
@HttpCode(HttpStatus.OK)
@Header('Content-Type', 'application/pdf')
@Header('Content-Disposition', 'attachment; filename=test.pdf')
pdf() {
    return createReadStream('./nodejs.pdf');
}

顺便说一句,我认为使用Stream 而不是readFile 应该更好。因为它将文件的所有内容加载到 RAM 中。

【讨论】:

  • 我复制了您的确切代码(并替换了文件名,显然)但结果仍然相同...您使用的是哪个版本的 Nest?
  • 我必须在创建的读取流上添加 .pipe(response) 才能使其工作(并删除 Content-Disposition 标头)
  • 对我不起作用,我使用 html-pdf 像缓冲区(甚至作为流)创建一个 pdf,标题为 content-type application/pdf 响应是空的,但是当我只设置 content-disposition 工作正常...
  • 问题出在 Postman,Insomia 或其他客户休息了,我终于可以查看回复并下载 pdf 成功
【解决方案2】:

我知道这个旧线程。但它可能会帮助某人。 类似于@Victor

  @Get('pdf')
  @HttpCode(201)
  @Header('Content-Type', 'image/pdf')
  @Header('Content-Disposition', 'attachment; filename=test.pdf')
  public pdf() {
    return fs.createReadStream('./test.pdf');
  }

【讨论】:

【解决方案3】:

您可以使用现成的装饰器@Res 这是我的工作解决方案:

控制器(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);
}

在我的情况下,流变量只是由 html-pdf 库创建的就绪流,因为我通过 html https://www.npmjs.com/package/html-pdf 创建 pdf,但您如何创建流并不重要。问题是您应该使用 @Res 装饰器并将其管道化,因为它的原生 NestJs 解决方案。

这里还有如何在客户端声明文件的代码: https://gist.github.com/javilobo8/097c30a233786be52070986d8cdb1743

不管怎样,让我们​​在你的情况下试试这个:

@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);
  }
}

【讨论】:

  • 做得很好。这涵盖了@Res 的简单使用和在响应中设置标题,但是我需要使用 res.headers.set。否则美丽!将此与下一篇带有文件附件的帖子结合起来,非常完美。
  • 这是缺少的部分。响应需要从“express.js”而不是“@nestjs/common”导入。另外,然后使用 res.type() 和 res.attachment()。
【解决方案4】:

2021 年更新:

从现在开始,在 Nest 版本 8 中,您可以使用 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);
  }
}

更多信息请参阅 Nest 官方文档:https://docs.nestjs.com/techniques/streaming-files

【讨论】:

    【解决方案5】:

    也于 2021 年更新:

    我更喜欢这种方式,因为我不需要后控制器拦截器逻辑。 我们可以控制文件的名称,使其内联或下载文件。

    @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);
      }
    

    更多信息请参阅 Nest 官方文档:https://docs.nestjs.com/techniques/streaming-files

    【讨论】:

      【解决方案6】:

      不幸的是,“StreamableFile”可用于 v8 及更高版本。每次我查看 NestJS 时,它都不符合 NestJS 的需求和版本。这是一件可怕的事情......

      【讨论】:

        猜你喜欢
        • 2012-07-18
        • 1970-01-01
        • 2023-03-25
        • 1970-01-01
        • 1970-01-01
        • 2015-10-12
        • 2019-12-20
        • 2014-09-08
        相关资源
        最近更新 更多