I never realized downloading a file on a button click could be such a headache, but here we are. After some trial and error, I did what we do best as developers—find solutions.
Here’s the situation:
I was using S3 and CloudFront for video file delivery. The videos were encrypted and access-restricted on the backend to ensure no one could directly grab them from my S3 bucket. Users had the ability to watch and download videos.
Attempt 1: Basic Download Logic
Initially, I tried the typical approach:
const handleDownload = () => {
const link = document.createElement("a");
link.href = videoUrl;
link.download = `${title || "video"}-thook.mp4`; // Provide a default filename if `title` is unavailable.
document.body.appendChild(link);
link.click();
document.body.removeChild(link); // Cleanup after the download is triggered
};
This worked… sort of. Instead of downloading, it redirected to another page and started playing the video.
The culprit? I had set ContentType
during the S3 upload process.
const param = {
Bucket: process.env.AWS_S3_BUCKET_NAME,
Key: `${directory}/${uuid()}-${file.originalname}`,
Body: file.buffer,
// ContentType: file.mimetype ❌ (Removed this)
};
await s3.upload(param).promise();
After removing the ContentType
setting, files finally started downloading correctly on desktops.
Attempt 2: Mobile Safari and Other Browser Issues
The next problem? Safari on mobile devices and some other browsers still insisted on opening the video instead of downloading it.
To fix this, I decided to stream the video from the backend using Node.js and set appropriate headers to force downloads:
import axios from "axios";
const downloadWithSignedURL = async (req: Request, res: Response) => {
const signedUrl = req.query.signedUrl as string;
const filename = (req.query.filename as string) || "video.mp4";
if (!signedUrl) {
return res.status(400).send("Signed URL is required.");
}
try {
// Fetch the video stream using axios
const response = await axios.get(signedUrl, {
responseType: "stream",
});
if (response.status === 200) {
// Set headers to force download
res.setHeader("Content-Disposition", `attachment; filename="${filename}-THOOK.mp4"`);
res.setHeader("Content-Type", "video/mp4");
// Pipe the video stream to the client
response.data.pipe(res);
} else {
res.status(response.status || 500).send("Failed to fetch video.");
}
} catch (error) {
console.error("Error handling download:", error);
res.status(500).send("Failed to handle download.");
}
};
//route.ts
...
router.get("/downloadVideo", DownloadController.downloadWithSignedURL);
On the frontend, I adjusted the logic to interact with this backend route:
//in react
const handleDownload = () => {
const downloadUrl = `${API_ROUTE}/downloadVideo?signedUrl=${encodeURIComponent(url)}&filename=${encodeURIComponent(name || "video.mp4")}`;
window.open(downloadUrl, "_blank");
};
Final Solution: Seamless Download Without a New Tab
To improve user experience, I modified the logic to directly trigger downloads without opening a new tab:
const handleDownload = () => {
if (!url) {
alert("Please provide the signed URL.");
return;
}
try {
const downloadUrl = `${API_ROUTE}/downloadVideo?signedUrl=${encodeURIComponent(url)}&filename=${encodeURIComponent(name || "video.mp4")}`;
const link = document.createElement("a");
link.href = downloadUrl;
link.setAttribute("download", name || "video.mp4");
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
console.error("Download error:", error);
}
};
And voilà! The download now starts immediately after clicking the button, with no interruptions.
So that is how I created the download feature you here:
Side Notes
Here are a few key insights from the process:
S3 ContentType Issues: Setting
ContentType
caused files always be in the specified format instead of downloading. Removing it fixed desktop downloads.Browser Compatibility: Safari and some browsers required backend streaming with forced download headers for consistency.
Better UX: Dynamically creating an
<a>
element allowed downloads without opening new tabs, creating a smoother user experience.
Thanks for reading 🙇♂️.
Top comments (0)