Alex
Exact same situation, very useful, cheers!
I've been working on a small application allowing users to upload media to a Cloudflare R2 bucket. The high-level stack is pretty simple. It's got a client-side piece (React), a middle-tier service (Fastify on Node), with Cloudflare backing the uploads.
To no surprise, downloading that content is a part of the work too. When I began thinking through this, I was planning on streaming the objects through my Fastify server, and then straight to the users' browsers. But then I remembered: there's an very handy feature built into S3 (and therefore R2, which implements the same API).
That feature is presigned URLs. When you generate one and hand it out, that person gets time-limited permission to access the file. It'd be a far simpler means of getting a file out of my bucket and onto a user's device.
Using a presigned URL would save me a lot of time (and other resources), but I didn't want the user to click the link and navigate away from the page to access the object. So, I opted to use the download
attribute available on the <a>
tag. Click such a link, and the browser will prompt you to save it to your machine.
<a href="http://my-site.com/resource.mp3" download="file-name.mp3">
Download Audio
</a>
I expected it to just work, but it didn't. Instead, it navigated to a new page, showing the resource directly in the browser.
I was confused, especially because some pretty good sources out there had no mention of why the browser might decide to honor or ignore the download request. Even MDN was pretty... vague about its reliability:
After some digging, that confusion moved toward clarity when I came across this in the HTML spec:
In cross-origin situations, thedownload
attribute has to be combined with the `Content-Disposition
` HTTP header [...] to avoid the user being warned of possibly nefarious activity.
And it was all solidified after seeing notes on this particular Chrome feature: browsers will ignore the download
for cross-origin resources.
That explained my application's behavior. The presigned URLs came from R2 – not from my React application's domain. As a result, no download was triggered. The resource was simply treated as another navigation destination.
As you might've caught above, the solution to this is simple: set the Content-Disposition
header on the resource to attachment
. This will signal to the browser that it should download the resource – not navigate to it.
Fortunately, the AWS SDK allows you to set custom response headers when you generate a presigned URL. I'm using Node, so that meant using the ResponseContentDisposition
property when retrieving the object.
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const command = new GetObjectCommand({
Bucket: "the-bucket-name",
Key: "the-file-key",
+ // Customize the `Content-Disposition` header!
+ ResponseContentDisposition: `attachment; filename="file-name.mp3"`,
});
const signedUrl = getSignedUrl(S3, command, { expiresIn: 3600 });
This'll give behavioral instructions to the browser, as well as provide a default name for the file when it's downloaded. I get exactly the user experience I want, and I still don't need to mess with streaming anything through my server. Cheaper, easier, and more secure.
It might be worth calling out that after I set that Content-Disposition
header, the download
attribute didn't have any impact on what happened when my link was clicked. It would now always trigger the download.
Still, I can see it being useful in keeping around. It'll better signal a link's purpose to other client-side code (maybe you'd like to style download
links differently). So, despite it not bring strictly necessary, I'll probably keep using it. I like clarity.
There's still a small part of me kicking myself for taking so long to realize this pretty critical piece of the download
attribute. But at the same time, I know I'm not the only one who has these moments, so I'm sure the feeling will quickly pass. If anything, let this be an encouragement to share even the smallest bits of learnings you come across out there. People like me might benefit from it.
Get blog posts like this in your inbox.
May be irregular. Unsubscribe whenever.Exact same situation, very useful, cheers!
I had the exact same problem, and this post helps a lot,
thank you so much
Glad it was helpful, Ivan 👍
Im using google cloud storage - as I understand it's all of the same machinery
I have a gzip, I sign it, I set the metadata to have content disposition of "attachment"
Still, the anchor just opens the link and no download is initiated.
This was very useful. I was wondering why the download attribute wasn't working for me.
Happy to help, Mr. Nutz!
Thank you for this - I've been struggling with file downloads from my cloudflare bucket for a while, and you genuinely saved my bacon.
Thanks again!
Glad it was helpful!
Thank you for this article, my problem was exactly the same, trying to make a download button from a presigned url from a s3 bucket. I had no idea why the page was loading the image. Now I realize that I need to study about CORS. The interesting thing though is that the guides I saw about using the download don't mention cors