<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Usman Farooqi</title>
    <description>The latest articles on DEV Community by Usman Farooqi (@usman_farooqi_f3ef266029b).</description>
    <link>https://dev.to/usman_farooqi_f3ef266029b</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4237683%2F9415fb21-3724-4508-9b5a-4c8ab30b28a9.png</url>
      <title>DEV Community: Usman Farooqi</title>
      <link>https://dev.to/usman_farooqi_f3ef266029b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/usman_farooqi_f3ef266029b"/>
    <language>en</language>
    <item>
      <title>Stop trusting file extensions: validating uploads by content (with ffmpeg)</title>
      <dc:creator>Usman Farooqi</dc:creator>
      <pubDate>Wed, 11 Nov 2026 17:24:49 +0000</pubDate>
      <link>https://dev.to/usman_farooqi_f3ef266029b/stop-trusting-file-extensions-validating-uploads-by-content-with-ffmpeg-4m0p</link>
      <guid>https://dev.to/usman_farooqi_f3ef266029b/stop-trusting-file-extensions-validating-uploads-by-content-with-ffmpeg-4m0p</guid>
      <description>&lt;p&gt;A user of my free tool uploaded a recording named &lt;code&gt;voice.m4a&lt;/code&gt; and got "This doesn't look like a real .m4a file." The file was fine. It was a WebM/Opus recording that had been saved with an &lt;code&gt;.m4a&lt;/code&gt; extension. My upload check was wrong, not the user.&lt;/p&gt;

&lt;p&gt;This is a short write-up of what I changed, and why file extensions are the wrong thing to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;My converter (Node + ffmpeg) checked the first bytes of an upload against the file's extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before: decide by extension, then check one signature&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"m4a"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"ftyp"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real files are renamed all the time: WebM saved as &lt;code&gt;.m4a&lt;/code&gt;, an MP3 named &lt;code&gt;.wav&lt;/code&gt;, a screen recording shared as &lt;code&gt;.mp4&lt;/code&gt;. ffmpeg itself does not care about the extension and detects the format from the content, so my stricter check only rejected good files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: sniff the content, accept any known media container
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isKnownMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ascii&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ISO base media (MP4, M4A, MOV, 3GP, HEIC): size + box type&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ftyp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"moov"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"mdat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"free"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"skip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"wide"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ascii&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ascii&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ID3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;ascii&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"RIFF"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;ascii&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"OggS"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;ascii&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"fLaC"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mh"&gt;0x1a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mh"&gt;0x45&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mh"&gt;0xdf&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mh"&gt;0xa3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Matroska/WebM&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mh"&gt;0xff&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xe0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mh"&gt;0xe0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// MP3/ADTS frame sync&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only binary signatures match. Plain text never does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just accept everything?
&lt;/h2&gt;

&lt;p&gt;Because ffmpeg is more powerful than it looks. A &lt;em&gt;text&lt;/em&gt; file can be an HLS playlist (&lt;code&gt;#EXTM3U&lt;/code&gt;) or a concat script (&lt;code&gt;ffconcat version 1.0&lt;/code&gt;) that points at other files or URLs. If you hand a user's text file to ffmpeg, it may read local files or fetch remote ones for them. Two cheap defences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reject text.&lt;/strong&gt; The signature check above never matches a playlist, because it starts with printable characters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrict protocols.&lt;/strong&gt; Pass &lt;code&gt;-protocol_whitelist file&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt;, so even a crafted input cannot reach the network.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ffmpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-hide_banner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-loglevel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-protocol_whitelist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-vn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-map"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"0:a:0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test with real files, not just unit tests
&lt;/h2&gt;

&lt;p&gt;The unit tests were green and the bug still shipped, because I never fed it a real renamed file. I now generate a matrix of real files with ffmpeg (AAC, Opus, MP3 with an ID3 tag, FLAC, WAV, HEVC MOV, VP9 WebM, and each one with the "wrong" extension), upload them to the running service and check the output. It found two more gaps (a tool that rejected audio files in a video tool) within minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Decide by content, not by name. Let the real decoder be the final judge.&lt;/li&gt;
&lt;li&gt;Allow-list binary signatures; never let text reach tools that interpret text (ffmpeg, ImageMagick, LibreOffice).&lt;/li&gt;
&lt;li&gt;Restrict what the tool may touch (&lt;code&gt;-protocol_whitelist&lt;/code&gt;, no network, size caps, per-IP rate limits).&lt;/li&gt;
&lt;li&gt;Keep original filenames only for display, and keep Unicode letters in them (I used &lt;code&gt;/[^\p{L}\p{N}._\- ]+/gu&lt;/code&gt;), or users with Urdu or Arabic file names get underscores.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use this in a free converter site with audio, video, PDF and image tools: &lt;a href="https://saveanyreel.com" rel="noopener noreferrer"&gt;saveanyreel.com&lt;/a&gt;. Bug reports with the file type that failed are very welcome.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a free video downloader and 50+ browser tools on Oracle's free tier</title>
      <dc:creator>Usman Farooqi</dc:creator>
      <pubDate>Tue, 10 Nov 2026 18:37:32 +0000</pubDate>
      <link>https://dev.to/usman_farooqi_f3ef266029b/building-a-free-video-downloader-and-50-browser-tools-on-oracles-free-tier-3976</link>
      <guid>https://dev.to/usman_farooqi_f3ef266029b/building-a-free-video-downloader-and-50-browser-tools-on-oracles-free-tier-3976</guid>
      <description>&lt;p&gt;I built &lt;a href="https://saveanyreel.com" rel="noopener noreferrer"&gt;SaveAnyReel&lt;/a&gt;, a free video downloader (Instagram, TikTok, YouTube, Facebook, Pinterest, Snapchat) with 50+ small tools next to it: PDF, image, audio and calculators. I wanted to run it at &lt;strong&gt;$0&lt;/strong&gt;, so everything lives on Oracle's Always Free ARM VM. Here is how it is put together and what broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16 (App Router) + React 19 + Tailwind v4&lt;/strong&gt; for the site and API routes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose&lt;/strong&gt; on one Always Free ARM VM: Caddy (HTTPS), the Next.js app, a worker, Redis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BullMQ + Redis&lt;/strong&gt; for the job queues. The API validates a link and enqueues a job; the worker does the heavy lifting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;yt-dlp + ffmpeg&lt;/strong&gt; in the worker. Videos are normalised to H.264 + AAC, because iPhones play VP9/AV1 MP4s as audio only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LibreOffice, poppler, ghostscript, libheif&lt;/strong&gt; in the worker for the document and HEIC converters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keep the server idle: do it in the browser
&lt;/h2&gt;

&lt;p&gt;Most tools never touch the server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDF merge, split, rotate and JPG to PDF use &lt;code&gt;pdf-lib&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Image resize, crop, compress and convert use the Canvas API.&lt;/li&gt;
&lt;li&gt;QR codes use &lt;code&gt;qrcode&lt;/code&gt;; OCR uses &lt;code&gt;tesseract.js&lt;/code&gt; (language data is downloaded once, the image never leaves the device).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only things that really need native tools (Word to PDF, HEIC, video to MP3) go through the queue, with size caps and a per-IP rate limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that bit me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. YouTube blocks datacenter IPs.&lt;/strong&gt; "Sign in to confirm you're not a bot" appeared for every client I tried. The fix that worked for me: a Cloudflare WARP SOCKS5 sidecar container, and only YouTube requests go through it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Do not trust file extensions.&lt;/strong&gt; My upload check only accepted a certain MP4 header for &lt;code&gt;.m4a&lt;/code&gt;. A real user uploaded a WebM/Opus recording saved as &lt;code&gt;.m4a&lt;/code&gt; and got "not a real file". I now sniff the &lt;em&gt;content&lt;/em&gt; for known media signatures. Plain text (HLS playlists, concat scripts) is always rejected, and ffmpeg gets &lt;code&gt;-protocol_whitelist file&lt;/code&gt;, so a crafted file cannot make it fetch URLs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Cache what the API just learned.&lt;/strong&gt; The app resolves a link, then the worker used to run yt-dlp's slow extraction again. Sharing the metadata through Redis for 10 minutes (plus fetching the video and audio streams in parallel) roughly halved job time: a short YouTube clip went from about 14 s to 6.5 s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Hydration and randomness.&lt;/strong&gt; A password generator that uses &lt;code&gt;crypto.getRandomValues&lt;/code&gt; cannot render on the server. I used &lt;code&gt;useSyncExternalStore&lt;/code&gt; with a &lt;code&gt;false&lt;/code&gt; server snapshot, so the first client render matches the HTML and the password appears right after hydration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. SEO is half the product.&lt;/strong&gt; Every tool has its own page with FAQ and HowTo schema, a canonical URL and a short guide. I submit URLs with IndexNow so Bing picks up changes quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;It only handles public content. Instagram sometimes revokes the logged-in session the extractor needs, so I treat it as flaky and surface clear errors instead of hiding them.&lt;/p&gt;

&lt;p&gt;If you want to poke at it: &lt;a href="https://saveanyreel.com" rel="noopener noreferrer"&gt;saveanyreel.com&lt;/a&gt;. Feedback on any tool is welcome, especially bug reports with the file type that failed.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
