You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Express返回Buffer生成PDF时下载接口被浏览器重复调用两次怎么办

问题背景
  • 服务基于Express框架搭建,核心逻辑为执行指定计算生成PDF文件,直接以Buffer形式通过流返回给客户端下载,服务端不做PDF持久化存储
  • 接口触发方式为用户直接在浏览器新标签页打开对应地址,无前端业务代码主动调用、无其他自动拉取逻辑
  • 异常表现:触发下载时浏览器会向该业务接口发起2次重复请求,导致PDF生成计算逻辑重复执行,响应耗时翻倍,单次请求最长耗时可达20秒
  • 已排查确认:两次请求均命中目标业务接口,排除favicon.ico等静态资源请求的干扰

原有问题代码

let buffer =  await createPdfBuffer() // 自定义PDF Buffer生成函数
const readStream = new stream.PassThrough();

readStream.end(buffer);
res.set('Content-disposition', 'attachment; filename=' + file.pdf);
res.set('Content-Type', 'application/pdf');
readStream.pipe(res);
根因说明
  1. 响应头缺失关键标识触发浏览器双请求机制:Chromium内核(Chrome、Edge等)浏览器在新标签页打开下载类链接时,会先发起第一次请求探测资源类型、长度信息,如果响应未明确返回Content-Length、Accept-Ranges头,浏览器会中断第一次探测请求,重新发起第二次完整请求拉取全量文件用于下载,这是重复请求的核心原因
  2. 响应头语法不规范:原有代码中Content-Disposition头的filename值未用双引号包裹,部分浏览器会解析失败,进一步触发重复请求
  3. 冗余流逻辑:对已生成的Buffer额外构造PassThrough流转,存在流事件时序异常的风险,可能导致浏览器判定第一次响应无效,触发重试
修复方案
  1. 补全核心响应头,明确告知浏览器文件长度、范围请求支持能力,从根源避免浏览器探测类重复请求
  2. 修正Content-Disposition头语法,filename值用双引号包裹
  3. 移除冗余的PassThrough流逻辑,生成的Buffer可直接通过res.send返回
  4. 可选增加短周期请求去重缓存,覆盖用户误点、浏览器插件触发等偶发重复请求场景,避免重复执行耗时的PDF计算逻辑

修复后基础实现

const buffer = await createPdfBuffer()
// 设置完整响应头
res.set({
  'Content-Disposition': `attachment; filename="file.pdf"`,
  'Content-Type': 'application/pdf',
  'Content-Length': buffer.length,
  'Accept-Ranges': 'bytes'
})
// 直接返回Buffer即可
res.send(buffer)

可选增强:短周期缓存防重复执行

如果PDF生成逻辑耗时很长,可增加一层内存缓存,对30秒内相同参数的请求直接返回已生成的结果,彻底避免重复计算:

const crypto = require('crypto')
// 缓存存储
const pdfCache = new Map()
// 缓存有效期30秒,足够覆盖浏览器重复请求、用户误点等场景
const CACHE_EXPIRE = 30 * 1000

// 接口逻辑中加入缓存判断
router.get('/download-pdf', async (req, res, next) => {
  try {
    // 生成唯一缓存key:根据用户标识+请求参数生成,避免不同用户的缓存串扰
    const cacheKey = crypto.createHash('md5')
      .update(`${req.ip}_${JSON.stringify(req.query)}_${req.user?.id || ''}`)
      .digest('hex')
    
    // 检查缓存是否有效
    const cacheItem = pdfCache.get(cacheKey)
    if (cacheItem && Date.now() - cacheItem.createTime < CACHE_EXPIRE) {
      res.set(cacheItem.headers)
      return res.send(cacheItem.buffer)
    }

    // 无有效缓存则正常生成PDF
    const buffer = await createPdfBuffer()
    const responseHeaders = {
      'Content-Disposition': `attachment; filename="file.pdf"`,
      'Content-Type': 'application/pdf',
      'Content-Length': buffer.length,
      'Accept-Ranges': 'bytes'
    }

    // 写入缓存
    pdfCache.set(cacheKey, {
      createTime: Date.now(),
      buffer: buffer,
      headers: responseHeaders
    })

    // 定期清理过期缓存,避免内存占用过高
    if (pdfCache.size > 100) {
      for (const [key, val] of pdfCache) {
        if (Date.now() - val.createTime > CACHE_EXPIRE) {
          pdfCache.delete(key)
        }
      }
    }

    res.set(responseHeaders)
    res.send(buffer)
  } catch (err) {
    next(err)
  }
})

内容的提问来源于stack exchange,提问作者mohammadtz55

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 08:31:08