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.
Triggering Downloads is... Quirky.
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, the
download
attribute has to be combined with theContent-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.
The Server Gets the Final Say
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.
Is the Attribute Still Necessary?
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.
Share Those TILs
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.
Top comments (0)