DEV Community

Cover image for Download Video from s3 with Cloudfront, nodejs and react

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