
You know that feeling when a project is 99% done, the charts look beautiful, the data is crisp, and then the client asks:
Bro, can we add an 'Export to Excel' button?
It sounds like a five minute task. You just create an endpoint, slap an <a href="..."> tag in your React app, and call it a day, right?
Well, not if you care about security.
I ran into this exact wall while building Jurter. Our API is strictly locked down with Authorization: Bearer headers. The problem is, standard browser links don't support headers.
You cannot add a custom header to an <a> tag or window.location. This leaves us with a dilemma. How do we trigger a file download on a secured API?
fetch and Blob it!If you Google this problem, the most common advice is. "Use fetch (or axios) to send the header, get the response as a Blob, and create a temporary object URL."
I tried this. Technically, it works. But for a production app, it introduced two major flaws that I couldn't accept:
fetch, the browser must download the entire file into Javascript memory before it can save it to the disk. If a user tries to export a massive yearly report (say, 50MB+), you risk freezing the UI or crashing the browser tab entirely.fetch, the user doesn't see the native browser download bar. They just stare at a spinner on your button. They have no idea if the download is 10% or 99% done. It feels broken.I wanted the native experience. Click the button, see the browser's download manager fly out, and handle files of any size without touching RAM.
But I also needed the security of my Bearer token.
I solved this using a strategy I call the "Temporary Handshake"
Instead of forcing headers into the download request, I split the process into two steps. Think of it like a coat check at a concert. You show your ID (Authentication) to get a paper ticket. Later, you just hand over the paper ticket to get your coat.
When a user clicks "Export" in my React app, I don't trigger the download yet. Instead, I fire a standard, authenticated POST request to my backend.
Since this is an AJAX request, it carries the user's Auth headers perfectly.
On the backend (Go/Fiber), I validate the user and generate a short-lived token (the ticket).
This part is crucial: I'm saving the UserId and the Date Filters inside the token store on the server. The token itself is just a random string. it contains no data to decode.
Once the frontend receives this token, the hard work is done. We have a valid "ticket". Now we can use the browser's native capabilities.
Because we use window.location, the browser handles the stream. The file goes directly to the hard drive. No RAM spikes, and the user gets their native progress bar.
You might ask: "Wait, isn't that a public link? Can't someone change the URL parameters?"
A malicious user might try to be clever. They might get a valid token, and then try to change the URL to access someone else's data:
.../export?token=VALID_TOKEN&user_id=ADMIN_ID
But my backend logic simply ignores the URL parameters during the download phase. It look up the token in memory and uses the UserID stored there.
We didn't avoid security, we just moved it. By decoupling the Authorization (getting the token) from the Action (downloading the file), we get the best of both worlds.
Sometimes, the best engineering solution is just a simple handshake.