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

