I learned this one the hard way. When I first built ramrom.io, my PHP download proxy worked fine in one sense: it fetched the video file and sent it back to the visitor. The problem was what the browser did with it. Instead of saving the file, Chrome opened the video in a new tab and played it. The visitor had to find the three-dot menu and pick "Save video as". Most people did not know that, and a tool that makes you hunt for the download is not much of a tool.
The fix is a single HTTP header: Content-Disposition. It tells the browser what to do with the response body. Send Content-Disposition: inline and the browser tries to display it. Send Content-Disposition: attachment and the browser shows a save dialogue instead. That is the whole trick, and it is the difference between a downloader and a video player.
What my proxy actually sends
My proxy is a small PHP script. It takes a file URL, fetches it with cURL, and streams the bytes back to the visitor. Before it sends the first byte, it sets a handful of headers. The important ones look like this:
header('Content-Type: video/mp4'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Content-Length: ' . $size); Content-Type tells the browser what the file is. Content-Disposition: attachment tells it to download rather than display. Content-Length lets the browser show a real progress bar instead of an endless spinner. None of these is clever on its own. Together they are what makes the download feel like a download.
Filenames are the fiddly part
The filename value looks simple until you meet a real video title. Titles have spaces, apostrophes, emojis, and non-English characters. A raw title in the header can break the download or produce a file called download with no name at all.
So I sanitise the title before it goes anywhere near a header. I strip characters that are not safe in a filename, collapse repeated spaces, and cap the length. For non-ASCII titles, the plain filename parameter is not enough, because some browsers expect it to be plain Latin text. The proper answer is the RFC 5987 form:
Content-Disposition: attachment; filename="video.mp4"; filename*=UTF-8''%D9%81%D9%8A%D8%AF%D9%8A%D9%88.mp4 The plain filename is the fallback for older browsers. The filename* value is the UTF-8, percent-encoded one that modern browsers use. Sending both covers nearly everyone, and nobody ends up with a file named after a percent-encoded mess.
Why I do this on my server, not in the link
You might wonder why the proxy needs to set this header at all. Why not just link straight to the file? Two reasons. First, the file usually lives on a provider's CDN, and I do not control the headers that CDN sends. Often it sends Content-Disposition: inline, or nothing at all, which is how you end up with a video playing in a tab. Second, the direct link is sometimes a signed URL that expires in minutes, so handing it to the visitor is fragile. Proxying through my own server means I control every header, and the visitor gets the same behaviour every time.
There is one case where I deliberately do not proxy. Some YouTube files come from a tunnel that returns empty responses to datacenter IPs, so my server cannot fetch them at all. For those I send the visitor straight to the file with a "direct" label, and I am honest on the page about what that means: the file may open in the browser instead of downloading, because I cannot set headers on a link I do not serve.
The lesson
Content-Disposition is a one-line header, and it took me an afternoon of confused testing to understand it. That is the pattern with a lot of this project: the visible feature is the easy part, and the behaviour lives in headers most people never see. If you are building any kind of download tool, set the header yourself, test it with a title full of awkward characters, and check it in more than one browser. Your users will never thank you for it. They will just click download and get a file, which is exactly the point.