Forcing a browser to download a file (HTML and server side)

📄 BetterWays.dev wiki page | 🕑 Last updated: Aug 28, 2022

When a user clicks a normal HTML link to a file, i.e.:

<a href="path/to/file.ext">

Browsers will usually try to open the file directly (inline) instead of opening the file download dialog.

You can use HTML5 download attribute to tell modern browsers to open the download dialog instead:

<a href="path/to/file.ext" download>

You can also specify the suggested filename the user will see in the download dialog:

<a href="path/to/file.ext" download="myfile.ext">

If you specify just the file name (without the extension), the extension will usually be inferred from the original filename (this example will still suggest "myfile.ext" in most browsers):

<a href="path/to/file.ext" download="myfile">

Limitations and workarounds

Nowadays, download attribute is supported in all mainstream browsers, but there's an important limitation: this attribute only works for same-origin URLs (protocol, port, and host have to match), and the blob: and data: schemes.

Besides the download attribute, there's not much else you can do to force this behavior from the client side, but you can on the server side.

Including the Content-Disposition header in the HTTP response will have a similar effect to download attribute (but without this limitation):

Content-Disposition: attachment; filename=file.ext

You'll also need to send the Content-type header and the actual file contents.

In PHP, that would look something like this:

header('Content-Disposition: attachment; filename="downloaded.pdf"');
header('Content-type: application/pdf');
readfile('test.pdf');

But to avoid the performance overhead, I recommend doing that on the webserver level instead of the app level.

nginx

For nginx this could be as simple as:

add_header Content-Disposition 'attachment; filename="file.ext"';

Note: nginx will usually send the correct Content-type header automatically, but you can override that if needed:

add_header Content-Type 'application/pdf'

For a more general case, you could send a generic application/octet-stream header and Content-Disposition without a filename:

location /myloc {
        if ($request_filename ~* ^.*?\.(pdf|zip|docx)$) {
            add_header Content-Disposition attachment;
            add_header Content-Type application/octet-stream;
        }
    }

Combination of download attribute and Content-Disposition header

If both the download attribute and Content-Disposition header are present and both define the filename, the filename defined in the header will have priority.

In the case of Content-Disposition: inline header (which tells the browser to display the contents inline - as a part of the page), the download attribute will have the priority (for the same-origin URLs).


Comments and suggestions

You can use this form to submit a suggestion or a comment.

If you find this site useful, please consider supporting it. Supporters also get access to some extras.