DEV Community

Cover image for Download Video from s3 with Cloudfront, nodejs and react
Chocoscoding - Oyeti Timileyin
Chocoscoding - Oyeti Timileyin

Posted on

Download Video from s3 with Cloudfront, nodejs and react

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 }; 
Enter fullscreen mode Exit fullscreen mode

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(); 
Enter fullscreen mode Exit fullscreen mode

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."); } }; 
Enter fullscreen mode Exit fullscreen mode
//route.ts ... router.get("/downloadVideo", DownloadController.downloadWithSignedURL); 
Enter fullscreen mode Exit fullscreen mode

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"); }; 
Enter fullscreen mode Exit fullscreen mode

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); } }; 
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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 (1)

Collapse
 
ayotomiwa_solarin_dbd7bc7 profile image
Ayotomiwa Solarin

Thanks for sharing!