DEV Community

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

Posted on

1 1 1

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 🙇‍♂️.

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (1)

Collapse
 
ayotomiwa_solarin_dbd7bc7 profile image
Ayotomiwa Solarin

Thanks for sharing!

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay