三星Galaxy手机Samsung Internet浏览器长时文件下载无提示问题
问题背景与现象
我们开发了一款带有API后端的单页应用,后端会根据请求动态生成文件并返回给前端。部分文件生成耗时极短,部分则需要5秒以上。目前遇到的核心问题:在三星Galaxy手机的Samsung Internet浏览器中,下载生成耗时超5秒的文件时,无法弹出保存提示。文件实际已完成下载,但设备上没有显示任何保存提示,导致用户无法打开或存储文件。
该下载功能在所有测试过的桌面浏览器,以及三星Galaxy手机的Chrome浏览器中均能正常工作,仅在Samsung Internet浏览器中失效。
设备与环境信息
- 机型:Samsung Galaxy S22+
- One UI版本:5.1
- Android版本:13
- Samsung Internet浏览器版本:21.0.0.41(当前最新版本)
复现步骤
我们提取项目核心代码制作了简易服务端与客户端用于复现问题,步骤如下:
- 克隆仓库
- 进入
server文件夹启动服务端,详细说明见该文件夹下的README - 进入
client-js文件夹启动客户端,详细说明见该文件夹下的README - 在电脑端访问
http://localhost:8081,可正常下载两个文件:file1即时返回,file2延迟6秒返回 - 使用同网络下的三星Galaxy手机,通过Samsung Internet浏览器访问
http://your-local-ip-address:8081(替换为电脑实际局域网IP),可正常下载file1,但点击下载file2后(等待6秒)无任何反应,浏览器未弹出打开或保存提示
调试方法
可通过以下步骤调试手机端客户端:
- 在三星Galaxy手机的开发者选项中开启
USB debugging:- 进入
Settings>About phone>Software information - 连续点击7次
Build number - 进入
Settings>Developer options - 开启
USB debugging
- 进入
- 使用Google Chrome进行检查:
- 通过USB线连接手机与电脑
- 在电脑端Chrome浏览器中访问
chrome://inspect/ - 在手机端确认权限请求
- 等待设备出现在电脑端Chrome中
- 在手机端Samsung Internet浏览器中访问
http://your-local-ip-address:8081 - 等待页面出现在电脑端Chrome中,点击
inspect
此时会打开新的Chrome窗口显示手机端客户端,可通过F12打开开发者工具分析网络请求与控制台输出,会发现file2已发送至客户端,但无提示弹出。
核心代码片段
服务端核心代码
function serveFile(res, filePath, fileName, contentType) { const contentDisposition = `attachment; filename="${encodeURIComponent(fileName)}"`; res.setHeader('Content-Disposition', contentDisposition); res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition'); res.setHeader('Content-Type', contentType); res.setHeader('Access-Control-Allow-Origin', '*'); const fileStream = fs.createReadStream(filePath); fileStream.pipe(res); } const server = http.createServer((req, res) => { if (req.url === '/file1') { const filePath = path.join(__dirname, 'file1.txt'); serveFile(res, filePath, 'file1.txt', 'text/plain'); } else if (req.url === '/file2') { setTimeout(() => { const filePath = path.join(__dirname, 'file2.txt'); serveFile(res, filePath, 'file2.txt', 'text/plain'); }, 6000); } else { res.statusCode = 404; res.end('Not found'); } });
JS客户端下载核心代码
async function downloadFile(endpoint) { console.log('downloadFile()'); console.log('endpoint', endpoint); const response = await fetch(endpoint); console.log('response', response); const blob = await response.blob(); console.log('blob', blob); const url = URL.createObjectURL(blob); console.log('url', url); const contentDispositionHeader = response.headers.get('Content-Disposition'); const fileName = getFileName(contentDispositionHeader); console.log('fileName', fileName); const downloadLinkTag = document.createElement('a'); downloadLinkTag.href = url; downloadLinkTag.download = fileName; console.log('before click'); downloadLinkTag.click(); console.log('after click'); setTimeout(() => URL.revokeObjectURL(url), 0); } function getFileName(contentDispositionHeader) { let fileName = contentDispositionHeader .split(';')[1] .split('=')[1]; if (fileName.startsWith('"')) { fileName = fileName.substring(1, fileName.length - 1); } if (fileName.endsWith('"')) { fileName = fileName.substring(0, fileName.length - 2); } return decodeURI(fileName); }
Angular客户端三种下载方式核心代码
private downloadFile(apiUrl: string): void { this.http .get(apiUrl, { responseType: 'blob', observe: 'response' }) .subscribe(response => { const fileName = this.getFileNameFromHeaders(response.headers); console.log('fileName', fileName); // 方式1:a标签 + Object URL console.log('approach #1'); const data = response.body; if (!data) { console.log('no data'); return; } console.log('data', data); const url = URL.createObjectURL(data); console.log('url', url); const link = document.createElement('a'); link.href = url; link.download = fileName; console.log('before click'); link.click(); console.log('after click'); setTimeout(() => URL.revokeObjectURL(url), 0); // 方式2:FileSaver.js console.log('approach #2'); const blob = new Blob([response.body as Blob], {type: 'text/plain'}); console.log('blob', blob); console.log('before saveAs'); saveAs(blob, fileName); console.log('after saveAs'); // 方式3:FileReader console.log('approach #3'); const reader = new FileReader(); reader.onloadend = function(e) { console.log('reader.result', reader.result); const link = document.createElement('a'); document.body.appendChild(link); link.href = reader.result as string; link.download = fileName; const clickEvent = new MouseEvent('click'); console.log('before dispatch click event'); link.dispatchEvent(clickEvent); console.log('after dispatch click event'); setTimeout(()=> { document.body.removeChild(link); }, 0) } console.log('response.body', response.body); console.log('before readAsDataURL'); reader.readAsDataURL(response.body as Blob); console.log('after readAsDataURL'); }); } private getFileNameFromHeaders(headers: HttpHeaders): string { const contentDisposition = headers.get('Content-Disposition'); if (!contentDisposition) { return 'unknown.txt'; } let fileName = contentDisposition .split(';')[1] .split('=')[1]; if (fileName.startsWith('"')) { fileName = fileName.substring(1, fileName.length - 1); } if (fileName.endsWith('"')) { fileName = fileName.substring(0, fileName.length - 2); } return decodeURI(fileName); }
仓库中还有一个简易Angular客户端,包含三种下载方式(a标签+Object URL、FileSaver.js、FileReader),均出现相同问题:延迟文件无下载提示。
补充发现
- 问题不仅出现在服务端响应超时(>5秒)的场景,在客户端模拟点击a标签前添加超5秒延迟也会触发,推测Samsung Internet浏览器在用户点击下载按钮后有5秒超时限制。
- 移除a标签的
download属性后,浏览器会显示文件内容,但不符合我们需要弹出保存提示的需求;使用const data = new Blob([data as Blob], {type: 'application/octet-stream'});可触发提示,但会丢失文件名,同样不可行。
疑问咨询
- 我们的实现是否存在错误?有无其他方法可让三星Galaxy手机上的下载提示正常显示?
- 这是否是Samsung Internet浏览器的bug?
内容的提问来源于stack exchange,提问作者TOG
相关产品推荐
相关产品推荐

