Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 3 min read

Setting the right MIME type when proxying MP4 and M4A through PHP

Setting the right MIME type when proxying MP4 and M4A through PHP When I first built the download proxy for ramrom.io, the files arrived fine but the browser did not know what to do with them. A video would open in a b

Setting the right MIME type when proxying MP4 and M4A through PHP

When I first built the download proxy for ramrom.io, the files arrived fine but the browser did not know what to do with them. A video would open in a blank tab instead of downloading. An audio file would play in the page, or worse, show a page of gibberish characters. The bytes were correct. The header was wrong.

The problem is simple once you see it. PHP sends Content-Type: text/html by default. When my proxy fetched a video from a provider and streamed it straight to the visitor, that default header went along for the ride. The browser looked at the header, decided it was looking at a web page, and acted accordingly. The file extension in the URL did not save me, because the browser trusts the header more than the name.

So the proxy now sets the MIME type itself, based on the file it is serving, before a single byte of the file goes out. MP4 video gets video/mp4. That one is well known. The one that caught me out is M4A audio. My first instinct was audio/mpeg, because that is the MP3 type and M4A feels like the same kind of thing. It is not. M4A is an MP4 container holding AAC audio, so the correct type is audio/mp4. Sending audio/mpeg for an M4A file is wrong, and some players will refuse it or guess badly. Getting this one type right fixed a whole class of "the audio will not play" complaints in one go.

The code is short, and the order matters more than the length:

$mime = ($ext === 'm4a') ? 'audio/mp4' : 'video/mp4';
header('Content-Type: ' . $mime);
header('Content-Disposition: attachment; filename="' . $safeName . '"');
header('Content-Length: ' . $size);
header('X-Content-Type-Options: nosniff');
readfile($tmpFile);

A few things in there earned their place the hard way. Content-Disposition: attachment is what turns "open it" into "download it". Without it, a browser that can play MP4 will play it in the tab, and the visitor who wanted a file ends up watching it instead. The filename is sanitised first, because a video title can contain quotes and newlines, and a newline inside a header is a security hole, not a cosmetic bug.

Content-Length matters more than it looks. Without it, the browser cannot show a real progress bar or a real file size, so the download looks stuck at an unknown size. And X-Content-Type-Options: nosniff tells the browser not to second-guess the type I declared. I set the type deliberately, so I do not want the browser sniffing the first bytes and deciding it knows better.

The other rule is that headers must go out before any output at all. A single blank line before <?php, or one stray echo left in from debugging, and PHP has already sent its default headers. After that, every header() call fails silently in the worst way: the download still works, but with the wrong type, and you only find out when a visitor complains. I keep the proxy file free of closing ?> tags entirely so trailing whitespace cannot sneak in.

One more thing I do not do: I do not trust the provider's own Content-Type. Providers send all sorts, including application/octet-stream for everything and occasionally a plain wrong type. My proxy decides the type from the file extension of what it actually fetched, not from what the provider claimed. That way the type is consistent no matter which of my providers answered the request.

It is a small detail, and it took me longer to learn than it should have. But on a tool site, the download is the whole product. If the browser mishandles the file, the visitor does not think "the header was wrong". They think the tool is broken. Setting three headers correctly is the difference between those two reactions.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.