Context and Problem Statement
We are currently developing a single-page-application with an API backend. The API provides files to the single-page-application which are generated on-the-fly on request on the server. Some files take very little time to be generated, others take longer (> 5 seconds). Now we face the issue, that downloading those files which take more than 5 seconds to be generated, is not working in the "Samsung Internet" browser on a Samsung Galaxy phone. On such devices, the prompt to save the file is not shown. The file is indeed being downloaded, but the prompt to save the file on the device is just not shown. Like this, the users cannot open or store the file on these devices.
The download works in all desktop browsers which we tested and also in the Chrome browser on a Samsung Galaxy phone. It does just not work in the "Samsung Internet" browser on a Samsung Galaxy phone.
Version of my Samsung Galaxy phone and "Samsung Internet" browser:
How to Reproduce
We extracted the relevant code from our project and created a simple webserver and client to demonstrate the issue. You can reproduce the issue by following these steps:
server
folder and start the server on your computer. Please consult the README
file inside the server
folder for detailed instructions.client-js
folder and serve the client from your computer. Please consult the README
file inside the client-js
folder for detailed instructions.http://localhost:8081
. You should be able to download both files. File 1 is being served immediately and file 2 is being served with a delay of 6 seconds.http://your-local-ip-address:8081
(replace your-local-ip-address
with the actual local ip address of your computer). Here you should be able to download file 1. But if you try to download file 2, nothing happens (after 6 seconds). The "Samsung Internet" browser does not show a prompt to the user to open or store file 2 (as it does for file 1).How to Debug
You can debug the client running on your phone using the following steps:
USB debugging
in the Developer options
on your Samsung Galaxy phone:
Settings
> About phone
> Software information
Build number
Settings
> Developer options
USB debugging
chrome://inspect/
in the Google Chrome browser on your computer.http://your-local-ip-address:8081
(replace your-local-ip-address
with the actual local ip address of your computer) in the "Samsung Internet" browser on your phone.inspect
This opens a new Chrome browser windows which shows you the client running on your phone. Now you can open the Chrome developer tools (F12) and analyze the network traffic and the console output of the client. You will see that file 2 is indeed being sent to the client, but that no prompt is shown on the phone for file 2.
Code Snippets
All the relevant code to reproduce the issue is available in this Github repository: https://github.com/tobias-graf-p/file-download-issue
As a reference, I post the relevant pieces of code here:
Code to serve the files (server), full file
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');
}
});
Code to download the files (client), full file
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);
}
There is a second client (simple Angular app) available in the Github repository with which you also can reproduce the issue as well. This client contains even three different approaches to download the file (using an a-tag with object url, using FileSaver.js and using a FileReader), which all fail the same way (no prompt for the file with the delay).
Code of the three approaches, full file
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);
//
// Approach #1: a-tag with 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);
//
// Approach #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');
//
// Approach #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);
}
Additional Information
download
attribute from the a tag makes the "Samsung Internet" browser actually show the downloaded file. But it shows the content of the downloaded file in the browser window then, which is not an option for us. (We would like to prompt the users to open or store the file.) We can trick the "Samsung Internat" browser to show the prompt again with using const data = new Blob([data as Blob], {type: 'application/octet-stream'});
instead of const data = response.body;
but with this approach we loose the file name. (The browser now asks the user to store the file with the blob guid as the file name, which is not an option for us neither.)Questions
Samsung responded to my question in the Samsung Developers Forums and confirmed, that this behavior is indeed by design. A user can disable this behavior by disabling the option "Browsing privacy dashboard" > "Block automatic downloads" in the settings of the Samsung Internet browser.
Answer in the Samsung Developers Forums:
https://forum.developer.samsung.com/t/file-download-prompt-not-showing-in-samsung-internet-browser-app/25740
Answer from "Samsung Members":
This is a security patch to prevent the infinite automatic downloads. Chromium automatic downloads implementation is different from ours. Chromium stores automatic download permission on url basis.In case of Samsung Internet we have a common button in Browsing privacy dashboard. When the developer delays the download by 5 or more seconds, the boolean value has_gesture is false (because function HasTransientUserActivation() reset its value after a specific timeout) hence we block the download immediately. To overcome this we request the developer to either don't delay the downlead by 5 or more seconds or disable the block automatic download button in Browser Privacy Dashboard.