TIL: A Link’s Download Attribute Won’t “Just Work” for Cross-Origin Resources

Created on November 12, 2023 at 11:45 am

TIL ORG : A Link’s Download Attribute Won’t “ Just Work WORK_OF_ART ” for Cross-Origin Resources ORG I just realized that using the "download" attribute on HTML links works only for same-origin resources by default. Fortunately, the "fix" is pretty simple, as long as you have control over your server’s response headers.

I’ve been working on a small application allowing users to upload media to a Cloudflare ORG

R2 CARDINAL bucket. The high-level stack is pretty simple. It’s got a client-side piece (React), a middle-tier service ( Fastify on Node WORK_OF_ART ), with Cloudflare ORG 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 ORG (and therefore R2 CARDINAL , 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 ORG 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 the ` Content-Disposition ` ORG HTTP header […] to avoid the user being warned of possibly nefarious activity.

And it was all solidified after seeing notes on this particular Chrome ORG feature: browsers will ignore the download for cross-origin resources.

That explained my application’s behavior. The presigned URLs came from R2 CARDINAL – 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 LAW 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 ORG SDK allows you to set custom response headers when you generate a presigned URL. I’m using Node, so that meant using the ResponseContentDisposition FAC 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 CARDINAL });

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 ORG 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.

Connecting to blog.lzomedia.com... Connected... Page load complete